Compare commits

...

152 Commits

Author SHA1 Message Date
nym21 1e0a7d6d0f release: v0.3.5 2026-06-23 18:54:46 +02:00
nym21 565251f12f docs: update generated docs 2026-06-23 18:54:14 +02:00
nym21 9879a986aa fixes 2026-06-23 18:47:51 +02:00
nym21 8d7ef8da61 fixes 2026-06-23 18:42:28 +02:00
nym21 17478a4ac4 website: redesign part 34 2026-06-22 16:42:14 +02:00
nym21 b3031b3375 website: redesign part 33 2026-06-21 17:12:25 +02:00
nym21 2e401379a0 deps: bumped 2026-06-21 17:11:43 +02:00
nym21 45ab6ebf71 website: redesign part 32 2026-06-19 23:16:27 +02:00
nym21 00f7d69ea6 website: redesign part 31 2026-06-18 22:39:28 +02:00
nym21 408d83c350 global: private xpub support part 3 2026-06-17 21:23:26 +02:00
nym21 43df9e098c global: private xpub support part 2 2026-06-17 11:25:42 +02:00
nym21 0c7861071d global: private xpub support part 1 2026-06-16 23:37:03 +02:00
nym21 6f430bdb8c clients: bump versions 2026-06-15 16:24:56 +02:00
nym21 4b415b215d release: v0.3.4 2026-06-14 00:46:39 +02:00
nym21 8614e9eded docs: update generated docs 2026-06-14 00:46:07 +02:00
nym21 c85da92cbc global: add cohorts by entry 2026-06-14 00:40:18 +02:00
nym21 297fc3b855 website: redesign part 30 2026-06-12 12:25:06 +02:00
nym21 c9d5a62fcb website: redesign part 29 2026-06-09 17:01:28 +02:00
nym21 90b3b51c48 website: redesign part 28 2026-06-09 16:12:50 +02:00
nym21 5966ab05e4 website: redesign part 27 2026-06-09 13:35:21 +02:00
nym21 c3506339cd website: redesign part 26 2026-06-09 11:26:19 +02:00
nym21 e54843291e website: redesign part 25 2026-06-09 11:26:14 +02:00
nym21 b0b261fe9f website: redesign part 24 2026-06-08 16:37:53 +02:00
nym21 6786be296d website: redesign part 23 2026-06-08 16:08:52 +02:00
nym21 e5068bbbf3 website: redesign part 24 2026-06-07 23:11:18 +02:00
nym21 36cfe49b20 website: redesign part 23 2026-06-07 16:46:53 +02:00
nym21 33cc13708a website: redesign part 22 2026-06-07 16:11:42 +02:00
nym21 2389632812 website: redesign part 21 2026-06-07 13:10:23 +02:00
nym21 e0bcdb8105 website: redesign part 20 2026-06-07 12:22:49 +02:00
nym21 45e83c98b9 website: redesign part 19 2026-06-07 11:14:15 +02:00
nym21 753bbf3e7e website: redesign part 18 2026-06-07 11:07:15 +02:00
nym21 54cc0cb446 website: redesign part 17 2026-06-07 11:01:31 +02:00
nym21 d64dcb75a9 website: redesign part 16 2026-06-07 01:44:52 +02:00
nym21 f599115f6c website: redesign part 15 2026-06-07 01:38:38 +02:00
nym21 9fc45625ad website: redesign part 14 2026-06-07 01:05:04 +02:00
nym21 c68d1d1fda website: redesign part 13 2026-06-07 00:54:50 +02:00
nym21 6cbe09af23 release: v0.3.3 2026-06-06 22:37:40 +02:00
nym21 96d35d1d29 docs: update generated docs 2026-06-06 22:37:07 +02:00
nym21 e23554811b website: redesign part 12 2026-06-06 22:30:13 +02:00
nym21 041c542046 website: redesign part 11 2026-06-06 22:29:33 +02:00
nym21 66dc7cd8f5 website: redesign part 10 2026-06-06 11:03:55 +02:00
nym21 b00692249c website: redesign part 8 2026-06-05 18:12:46 +02:00
nym21 ff2c04a100 website: redesign part 7 2026-06-05 16:03:04 +02:00
nym21 7cee0e2c5a clients: bump versions 2026-06-04 19:12:58 +02:00
nym21 744032f1f1 release: v0.3.2 2026-06-04 18:56:31 +02:00
nym21 99b171bad6 docs: update generated docs 2026-06-04 18:56:00 +02:00
nym21 37e2b6eae2 changelog: updated 2026-06-04 18:50:35 +02:00
nym21 a967fe8f35 oracle: changes + changelog: updated 2026-06-04 18:35:48 +02:00
nym21 a3f3c54675 oracle: v4 2026-06-04 15:38:01 +02:00
nym21 f41874f438 website: redesign part 6 2026-06-03 18:07:11 +02:00
nym21 98bbfec525 website: redesign part 5 2026-06-03 16:50:52 +02:00
nym21 1bcf3235b6 website: redesign part 4 2026-06-03 16:37:00 +02:00
nym21 07734b8bab website: redesign part 3 2026-06-03 16:26:55 +02:00
nym21 a2fd1e03ad website: redesign part 2 2026-06-03 12:41:26 +02:00
nym21 90e8741fb7 website: redesign part 1 2026-06-03 12:34:05 +02:00
nym21 5f5563fece docs: renamed claude to ai 2026-06-02 12:06:50 +02:00
nym21 c7edfce481 changelog: updated 2026-06-02 09:27:56 +02:00
nym21 7b3dd83b93 clients: bump versions 2026-06-01 20:22:13 +02:00
nym21 cae16227fd release: v0.3.1 2026-06-01 19:19:13 +02:00
nym21 dc2ca0ca27 docs: update generated docs 2026-06-01 19:18:42 +02:00
nym21 d161462137 deps: bumped 2026-06-01 18:10:24 +02:00
nym21 be20633945 heatmaps: part 23 2026-06-01 18:03:41 +02:00
nym21 2bbc535b58 heatmaps: part 22 2026-06-01 17:54:42 +02:00
nym21 88c38e74f9 heatmaps: part 21 2026-06-01 14:25:21 +02:00
nym21 a61b76a4a5 heatmaps: part 20 2026-06-01 13:31:00 +02:00
nym21 46b888337c heatmaps: part 19 2026-06-01 13:20:34 +02:00
nym21 4b49a04186 heatmaps: part 18 2026-06-01 13:03:45 +02:00
nym21 15b0cd2445 heatmaps: part 17 2026-06-01 13:03:39 +02:00
nym21 76720434d7 heatmaps: part 16 2026-06-01 12:19:32 +02:00
nym21 200cd1011e heatmaps: part 15 2026-06-01 12:04:44 +02:00
nym21 cb9f277d49 heatmaps: part 14 2026-06-01 12:01:24 +02:00
nym21 102933b406 heatmaps: part 13 2026-06-01 11:17:00 +02:00
nym21 e64ffac8d1 heatmaps: part 12 2026-06-01 10:56:58 +02:00
nym21 a94d31dfdf heatmaps: part 12 2026-06-01 10:30:44 +02:00
nym21 087a3b6fd6 heatmaps: part 11 2026-06-01 01:04:14 +02:00
nym21 7181d59966 heatmaps: part 10 2026-06-01 00:38:12 +02:00
nym21 3b7734a61a heatmaps: part 9 2026-05-31 23:35:19 +02:00
nym21 7860c5a8bd heatmaps: part 8 2026-05-31 18:57:23 +02:00
nym21 5df399d2f7 heatmaps: part 7 2026-05-31 12:05:48 +02:00
nym21 b2345db279 heatmaps: part 6 2026-05-31 01:38:50 +02:00
nym21 7e2fc8b455 heatmaps: part 5 2026-05-30 15:43:59 +02:00
nym21 c1ff095e4b heatmaps: part 5 2026-05-30 13:16:22 +02:00
nym21 cc8fde59e8 heatmaps: part 4 2026-05-30 11:36:49 +02:00
nym21 e43b53b429 heatmaps: part 3 2026-05-30 11:36:46 +02:00
nym21 6938204a24 heatmaps: part 2 2026-05-29 23:17:39 +02:00
nym21 52883bbdba heatmaps: part 1 2026-05-28 21:58:54 +02:00
nym21 100495fdba deps: bumped 2026-05-27 19:41:37 +02:00
nym21 0ad5be6974 global: snap 2026-05-26 15:33:22 +02:00
nym21 66037c862f global: added support for oracle histograms 2026-05-25 16:44:09 +02:00
nym21 ee20175cbf oracle: cleanup + split lib.rs 2026-05-24 18:40:35 +02:00
nym21 7ad0adf659 oracle: start at 340k 2026-05-24 11:34:57 +02:00
nym21 6219d2301d oracle: snap pre 340k patch 2026-05-24 00:07:25 +02:00
nym21 0aaffc6c43 oracle: doc fixes 2026-05-23 12:18:34 +02:00
nym21 9c74881c5d oracle: snapshot + start at 508k 2026-05-23 00:45:37 +02:00
nym21 bf8de73541 oracle: cleanup 2026-05-22 11:02:56 +02:00
nym21 56e8103178 clients: bump versions 2026-05-22 11:02:12 +02:00
nym21 773c0d090b website: cleanup & fixes 2026-05-22 11:01:34 +02:00
nym21 d6f4c0ac19 changelog: updated 2026-05-22 11:01:07 +02:00
nym21 0552ba60d2 release: v0.3.0 2026-05-18 19:43:54 +02:00
nym21 ff056587f7 docs: update generated docs 2026-05-18 19:43:22 +02:00
nym21 0b871e8600 global: snap 2026-05-18 19:38:05 +02:00
nym21 0bdca9086a docs: moved license 2026-05-18 11:40:28 +02:00
nym21 bbab864ed9 actions: remove release pipeline 2026-05-18 11:38:11 +02:00
nym21 d1b328e658 release: v0.3.0-beta.11 2026-05-17 22:31:22 +02:00
nym21 df0a482f8d docs: update generated docs 2026-05-17 22:31:01 +02:00
nym21 6ff43c0f74 scripts: fix quick release 2026-05-17 22:28:45 +02:00
nym21 4daabcee2c release: v0.3.0-beta.10 2026-05-17 22:20:30 +02:00
nym21 a6021b26cc docs: update generated docs 2026-05-17 22:19:58 +02:00
nym21 1a706da13c deps: bumped 2026-05-17 22:14:05 +02:00
nym21 20c4a113c9 oracle: v2 2026-05-17 22:13:03 +02:00
nym21 e5819769e8 website: snap 2026-05-17 22:12:44 +02:00
nym21 421e5286ce global: snap 2026-05-15 23:53:55 +02:00
nym21 68db22b9e8 mempool: polish/cleanup 2026-05-14 23:29:10 +02:00
nym21 90aca2e048 mmpl: new, mempool + rpc: fixes 2026-05-14 13:59:15 +02:00
nym21 528c134f26 mempool: fixes 2026-05-13 18:36:02 +02:00
nym21 5cc3fbfa6e crates: snapshot 2026-05-12 22:33:09 +02:00
nym21 8fc2e71492 website: snap 2026-05-12 22:32:53 +02:00
nym21 445c60a6f1 mempool: fixes 2026-05-10 19:40:02 +02:00
nym21 dd6eca138b mempool: fixes 2026-05-10 16:23:06 +02:00
nym21 774580ee11 mempool: fixes 2026-05-10 14:04:08 +02:00
nym21 fe5f30bca6 mempool: snap 2026-05-10 00:24:02 +02:00
nym21 c52a076bfc mempool: fix 2026-05-09 13:36:06 +02:00
nym21 e62b0ac2a5 global: next block template (+ diff) 2026-05-09 12:56:11 +02:00
nym21 3f2b5d3084 mempool: cleanup 2026-05-08 12:26:01 +02:00
nym21 aab16f8832 clients: bump versions 2026-05-08 12:14:34 +02:00
nym21 a9c0a09191 release: v0.3.0-beta.9 2026-05-08 11:56:58 +02:00
nym21 948a7cdd88 docs: update generated docs 2026-05-08 11:56:30 +02:00
nym21 25b2268563 mempool: fixes 2026-05-08 11:51:44 +02:00
nym21 dd88996f7f release: v0.3.0-beta.8 2026-05-08 00:36:19 +02:00
nym21 1643cf86ed docs: update generated docs 2026-05-08 00:35:52 +02:00
nym21 6e8be1af22 bindgen: js fix name of txid[] field name 2026-05-08 00:32:46 +02:00
nym21 9d18e2db9b mempool: fixes 2026-05-08 00:16:37 +02:00
nym21 d2b8992932 mempool: cleanups 2026-05-07 18:56:44 +02:00
nym21 f4910efd7d mempool: use bitcoin projected block, rest is a very simple prediction 2026-05-07 18:30:26 +02:00
nym21 1b39d21bbe blk: fix 2026-05-07 14:12:50 +02:00
nym21 cc9ebfaf42 global + blk 2026-05-07 14:02:53 +02:00
nym21 9347b42c9a mempool: fixes 2026-05-07 11:21:37 +02:00
nym21 cb74087f27 mempool: fixes 2026-05-06 19:31:18 +02:00
nym21 086bfd9938 global: adding support for safe lengths 2026-05-06 15:33:07 +02:00
nym21 da7671744f deps: bumped 2026-05-05 23:22:10 +02:00
nym21 abcb238022 global: sigops 2026-05-04 19:06:41 +02:00
nym21 dc32bd480f global: fixes 2026-05-04 16:57:21 +02:00
nym21 4663d13194 global: fixes 2026-05-03 12:44:18 +02:00
nym21 9cb5f2c880 global: fixes 2026-05-03 00:57:22 +02:00
nym21 2b8a0a8cf7 global: fixes 2026-05-03 00:42:16 +02:00
nym21 6f879a5551 global: fixes 2026-05-01 19:14:15 +02:00
nym21 1068ad4e8f query: fixes 2026-04-30 19:19:09 +02:00
nym21 9b42b40a36 mempool: fixes 2026-04-30 12:38:34 +02:00
nym21 43f3be4924 global: fixes 2026-04-29 16:51:01 +02:00
nym21 a7e41df1c6 clients: add .len() 2026-04-29 12:06:22 +02:00
nym21 f1749472e7 mempool: general improvements 2026-04-28 18:46:37 +02:00
nym21 66494c081c global: fixes 2026-04-27 19:19:14 +02:00
1495 changed files with 61642 additions and 317482 deletions
-15
View File
@@ -1,15 +0,0 @@
name: Check outdated dependencies
on:
schedule:
- cron: "0 9 * * 1"
workflow_dispatch:
jobs:
outdated:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install cargo-outdated
- run: cargo outdated --exit-code 1 --depth 1
-296
View File
@@ -1,296 +0,0 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
+1
View File
@@ -18,6 +18,7 @@ _*
/*.py
/*.json
/*.html
!/btc-cycle-sim.html
/research
/filter_*
/heatmaps*
-1
View File
@@ -1 +0,0 @@
Codex will review your output once you are done.
Generated
+323 -394
View File
File diff suppressed because it is too large Load Diff
+34 -44
View File
@@ -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.3.0-beta.7"
package.version = "0.3.5"
package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk"
package.readme = "README.md"
@@ -28,9 +28,6 @@ lto = false
strip = false
inherits = "release"
[profile.dist]
inherits = "release"
[profile.profiling]
inherits = "release"
debug = true
@@ -38,55 +35,55 @@ debug = true
[workspace.dependencies]
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"] }
brk_alloc = { version = "0.3.0-beta.7", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.0-beta.7", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.0-beta.7", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.0-beta.7", path = "crates/brk_cli" }
brk_client = { version = "0.3.0-beta.7", path = "crates/brk_client" }
brk_cohort = { version = "0.3.0-beta.7", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.0-beta.7", path = "crates/brk_computer" }
brk_error = { version = "0.3.0-beta.7", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.0-beta.7", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.0-beta.7", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.0-beta.7", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.0-beta.7", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.0-beta.7", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.0-beta.7", path = "crates/brk_oracle" }
brk_query = { version = "0.3.0-beta.7", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.0-beta.7", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.0-beta.7", path = "crates/brk_rpc" }
brk_server = { version = "0.3.0-beta.7", path = "crates/brk_server" }
brk_store = { version = "0.3.0-beta.7", path = "crates/brk_store" }
brk_traversable = { version = "0.3.0-beta.7", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.0-beta.7", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.0-beta.7", path = "crates/brk_types" }
brk_website = { version = "0.3.0-beta.7", path = "crates/brk_website" }
bitcoin = { version = "0.32.10", features = ["serde"] }
brk_alloc = { version = "0.3.5", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.5", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.5", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.5", path = "crates/brk_cli" }
brk_client = { version = "0.3.5", path = "crates/brk_client" }
brk_cohort = { version = "0.3.5", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.5", path = "crates/brk_computer" }
brk_error = { version = "0.3.5", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.5", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.5", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.5", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.5", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.5", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.5", path = "crates/brk_oracle" }
brk_query = { version = "0.3.5", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.5", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.5", path = "crates/brk_rpc" }
brk_server = { version = "0.3.5", path = "crates/brk_server" }
brk_store = { version = "0.3.5", path = "crates/brk_store" }
brk_traversable = { version = "0.3.5", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.5", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.5", path = "crates/brk_types" }
brk_website = { version = "0.3.5", path = "crates/brk_website" }
byteview = "0.10.1"
color-eyre = "0.6.5"
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
corepc-types = { version = "0.12.0", features = ["std"], default-features = false }
corepc-types = { version = "0.15.0", features = ["std"], default-features = false }
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
fjall = "=3.0.4"
fjall = "3.1.5"
indexmap = { version = "2.14.0", features = ["serde"] }
jiff = { version = "0.2.24", features = ["perf-inline", "tz-system"], default-features = false }
jiff = { version = "0.2.29", features = ["perf-inline", "tz-system"], default-features = false }
owo-colors = "4.3.0"
parking_lot = "0.12.5"
pco = "1.0.1"
pco = "1.0.2"
rayon = "1.12.0"
rustc-hash = "2.1.2"
schemars = { version = "1.2.1", features = ["indexmap2"] }
serde = "1.0.228"
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.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"] }
serde_json = { version = "1.0.150", features = ["float_roundtrip", "preserve_order"] }
smallvec = "1.15.2"
tokio = { version = "1.52.3", features = ["rt-multi-thread"] }
tower-http = { version = "0.7.0", 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.10.2", features = ["derive", "serde_json", "pco", "schemars"] }
vecdb = { version = "0.10.3", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release]
@@ -95,10 +92,3 @@ 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"
ci = "github"
allow-dirty = ["ci"]
installers = []
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 bitcoinresearchkit, kibo.money, satonomics
Copyright (c) 2025 Bitcoin Research Kit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+2
View File
@@ -0,0 +1,2 @@
*.md
!README.md
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "blk"
description = "A CLI to inspect Bitcoin Core blocks"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
bitcoin = { workspace = true }
brk_error = { workspace = true }
brk_reader = { workspace = true }
brk_rpc = { workspace = true }
brk_types = { workspace = true }
owo-colors = { workspace = true, features = ["supports-colors"] }
serde_json = { workspace = true }
[[bin]]
name = "blk"
path = "src/main.rs"
+27
View File
@@ -0,0 +1,27 @@
# blk
A CLI to inspect Bitcoin Core blocks.
Reads `blk*.dat` files directly via [`brk_reader`](../brk_reader) and resolves
the chain tip / heights via the Bitcoin Core RPC. Output is shell-friendly:
bare values, NDJSON, pretty JSON, or TSV.
## Install
```sh
cargo install --path crates/blk
```
## Quick start
```sh
blk 800000 hash # bare hash
blk 800000 height hash time # one compact JSON line
blk 800000 tx.0.vout.0.value # coinbase output 0 sats
blk 0..2 hash tx.0.txid # 3 NDJSON lines
blk tip tx.0 # whole coinbase tx as JSON
```
## Reference
Run `blk --help` for the full field/selector/option reference.
+132
View File
@@ -0,0 +1,132 @@
use std::{collections::HashSet, path::PathBuf};
use brk_error::{Error, Result};
use brk_rpc::{Auth, Client};
use crate::path::Path;
pub struct Args {
pub selector: String,
pub paths: Vec<Path>,
pub pretty: bool,
pub compact: bool,
bitcoindir: Option<PathBuf>,
blocksdir: Option<PathBuf>,
rpcconnect: Option<String>,
rpcport: Option<u16>,
rpccookiefile: Option<PathBuf>,
rpcuser: Option<String>,
rpcpassword: Option<String>,
}
impl Args {
pub fn parse(raw: Vec<String>) -> Result<Self> {
let mut pretty = false;
let mut compact = false;
let mut bitcoindir = None;
let mut blocksdir = None;
let mut rpcconnect = None;
let mut rpcport = None;
let mut rpccookiefile = None;
let mut rpcuser = None;
let mut rpcpassword = None;
let mut positional: Vec<String> = Vec::new();
let mut iter = raw.into_iter();
while let Some(a) = iter.next() {
if a == "-p" || a == "--pretty" {
pretty = true;
continue;
}
if a == "-c" || a == "--compact" {
compact = true;
continue;
}
if let Some(rest) = a.strip_prefix("--") {
let (key, value) = match rest.split_once('=') {
Some((k, v)) => (k.to_string(), v.to_string()),
None => (
rest.to_string(),
iter.next()
.ok_or_else(|| Error::Parse(format!("--{rest} requires a value")))?,
),
};
match key.as_str() {
"bitcoindir" => bitcoindir = Some(PathBuf::from(value)),
"blocksdir" => blocksdir = Some(PathBuf::from(value)),
"rpcconnect" => rpcconnect = Some(value),
"rpcport" => {
rpcport = Some(value.parse().map_err(|_| {
Error::Parse(format!("--rpcport: '{value}' is not a valid port"))
})?);
}
"rpccookiefile" => rpccookiefile = Some(PathBuf::from(value)),
"rpcuser" => rpcuser = Some(value),
"rpcpassword" => rpcpassword = Some(value),
other => return Err(Error::Parse(format!("unknown flag --{other}"))),
}
continue;
}
if a.starts_with('-') {
return Err(Error::Parse(format!("unknown flag {a}")));
}
positional.push(a);
}
let mut iter = positional.into_iter();
let selector = iter
.next()
.ok_or_else(|| Error::Parse("missing selector".into()))?;
let paths: Vec<Path> = iter.map(|f| Path::parse(&f)).collect::<Result<_>>()?;
let mut seen = HashSet::with_capacity(paths.len());
for p in &paths {
if !seen.insert(p.raw.as_str()) {
return Err(Error::Parse(format!("duplicate field '{}'", p.raw)));
}
}
Ok(Self {
selector,
paths,
pretty,
compact,
bitcoindir,
blocksdir,
rpcconnect,
rpcport,
rpccookiefile,
rpcuser,
rpcpassword,
})
}
pub fn bitcoin_dir(&self) -> PathBuf {
self.bitcoindir
.clone()
.unwrap_or_else(Client::default_bitcoin_path)
}
pub fn blocks_dir(&self) -> PathBuf {
self.blocksdir
.clone()
.unwrap_or_else(|| self.bitcoin_dir().join("blocks"))
}
pub fn rpc(&self) -> Result<Client> {
let host = self.rpcconnect.as_deref().unwrap_or("localhost");
let port = self.rpcport.unwrap_or(8332);
let url = format!("http://{host}:{port}");
let cookie = self
.rpccookiefile
.clone()
.unwrap_or_else(|| self.bitcoin_dir().join(".cookie"));
let auth = if cookie.is_file() {
Auth::CookieFile(cookie)
} else if let (Some(u), Some(p)) = (self.rpcuser.as_deref(), self.rpcpassword.as_deref()) {
Auth::UserPass(u.to_string(), p.to_string())
} else {
return Err(Error::Parse(
"no RPC auth: cookie file missing and --rpcuser/--rpcpassword not set".into(),
));
};
Client::new(&url, auth)
}
}
+407
View File
@@ -0,0 +1,407 @@
use std::cell::OnceCell;
use bitcoin::{
Address, Block, Network, ScriptBuf, Transaction, TxIn, TxOut, consensus::encode::serialize_hex,
hex::DisplayHex,
};
use brk_error::{Error, Result};
use brk_types::ReadBlock;
use serde_json::{Map, Value, json};
use crate::path::{Path, Step};
// `hex` is intentionally absent: matches `bitcoin-cli getblock <hash> 2`
// and keeps NDJSON dumps tractable. Still reachable explicitly via `blk N hex`.
const BLOCK_FIELDS: &[&str] = &[
"height",
"hash",
"version",
"version_hex",
"merkle",
"time",
"nonce",
"bits",
"difficulty",
"prev",
"txs",
"n_inputs",
"n_outputs",
"witness_txs",
"size",
"strippedsize",
"weight",
"subsidy",
"coinbase",
"coinbase_hex",
"header_hex",
"tx",
];
const TX_FIELDS: &[&str] = &[
"txid",
"wtxid",
"version",
"locktime",
"size",
"base_size",
"vsize",
"weight",
"inputs",
"outputs",
"is_coinbase",
"has_witness",
"is_rbf",
"total_out",
"hex",
"vin",
"vout",
];
const VIN_FIELDS: &[&str] = &[
"prev_txid",
"prev_vout",
"sequence",
"script_sig",
"script_sig_asm",
"witness",
"has_witness",
"is_rbf",
"coinbase",
];
const VOUT_FIELDS: &[&str] = &[
"value",
"script_pubkey",
"script_pubkey_asm",
"type",
"address",
];
pub struct Ctx<'a> {
block: &'a ReadBlock,
network: Network,
size_weight: OnceCell<(usize, usize)>,
}
impl<'a> Ctx<'a> {
pub fn new(block: &'a ReadBlock, network: Network) -> Self {
Self {
block,
network,
size_weight: OnceCell::new(),
}
}
pub fn resolve(&self, path: &Path) -> Result<Value> {
let (step, rest) = pop(&path.steps)?;
self.block_field(&step.name, step.index, rest)
}
pub fn resolve_str(&self, path: &Path) -> Result<String> {
Ok(match self.resolve(path)? {
Value::String(s) => s,
other => other.to_string(),
})
}
pub fn full(&self) -> Value {
let mut obj = Map::with_capacity(BLOCK_FIELDS.len());
for &name in BLOCK_FIELDS {
obj.insert(
name.into(),
self.block_field(name, None, &[]).expect("known block field"),
);
}
Value::Object(obj)
}
fn size_and_weight(&self) -> (usize, usize) {
*self
.size_weight
.get_or_init(|| self.block.total_size_and_weight())
}
fn block_field(&self, name: &str, index: Option<usize>, rest: &[Step]) -> Result<Value> {
let b = self.block;
let raw: &Block = b;
let scalar = |v| scalar_leaf(v, name, index, rest);
match name {
"height" => scalar(json!(*b.height())),
"hash" => scalar(json!(b.hash().to_string())),
"time" => scalar(json!(b.header.time)),
"version" => scalar(json!(b.header.version.to_consensus())),
"version_hex" => scalar(json!(format!(
"{:08x}",
b.header.version.to_consensus() as u32
))),
"bits" => scalar(json!(format!("{:08x}", b.header.bits.to_consensus()))),
"nonce" => scalar(json!(b.header.nonce)),
"prev" => scalar(json!(b.header.prev_blockhash.to_string())),
"merkle" => scalar(json!(b.header.merkle_root.to_string())),
"difficulty" => scalar(json!(b.header.difficulty_float())),
"txs" => scalar(json!(b.txdata.len())),
"n_inputs" => scalar(json!(
b.txdata.iter().map(|tx| tx.input.len()).sum::<usize>()
)),
"n_outputs" => scalar(json!(
b.txdata.iter().map(|tx| tx.output.len()).sum::<usize>()
)),
"witness_txs" => scalar(json!(
b.txdata.iter().filter(|tx| tx_has_witness(tx)).count()
)),
"size" => scalar(json!(self.size_and_weight().0)),
"weight" => scalar(json!(self.size_and_weight().1)),
"strippedsize" => {
let (size, weight) = self.size_and_weight();
scalar(json!((weight - size) / 3))
}
"subsidy" => scalar(json!(subsidy_sats(*b.height()))),
"header_hex" => scalar(json!(serialize_hex(&b.header))),
"hex" => scalar(json!(serialize_hex(raw))),
"coinbase" => scalar(json!(b.coinbase_tag().as_str())),
"coinbase_hex" => {
debug_assert!(
!b.txdata.is_empty() && !b.txdata[0].input.is_empty(),
"consensus-valid block has a coinbase tx with at least one input"
);
scalar(json!(b.txdata[0].input[0].script_sig.to_hex_string()))
}
"tx" => pick(&b.txdata, name, index, |i, tx| {
self.resolve_tx(tx, i == 0, rest)
}),
other => Err(unknown("block", other)),
}
}
fn resolve_tx(&self, tx: &Transaction, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
if steps.is_empty() {
let mut obj = Map::with_capacity(TX_FIELDS.len());
for &name in TX_FIELDS {
obj.insert(
name.into(),
self.tx_field(tx, is_coinbase, name, None, &[])
.expect("known tx field"),
);
}
return Ok(Value::Object(obj));
}
let (step, rest) = pop(steps)?;
self.tx_field(tx, is_coinbase, &step.name, step.index, rest)
}
fn tx_field(
&self,
tx: &Transaction,
is_coinbase: bool,
name: &str,
index: Option<usize>,
rest: &[Step],
) -> Result<Value> {
let scalar = |v| scalar_leaf(v, name, index, rest);
match name {
"txid" => scalar(json!(tx.compute_txid().to_string())),
"wtxid" => scalar(json!(tx.compute_wtxid().to_string())),
"version" => scalar(json!(tx.version.0)),
"locktime" => scalar(json!(tx.lock_time.to_consensus_u32())),
"size" => scalar(json!(tx.total_size())),
"base_size" => scalar(json!(tx.base_size())),
"vsize" => scalar(json!(tx.vsize())),
"weight" => scalar(json!(tx.weight().to_wu())),
"inputs" => scalar(json!(tx.input.len())),
"outputs" => scalar(json!(tx.output.len())),
"is_coinbase" => scalar(json!(is_coinbase)),
"has_witness" => scalar(json!(tx_has_witness(tx))),
"is_rbf" => scalar(json!(tx_is_rbf(tx))),
"total_out" => scalar(json!(tx_total_out(tx))),
"hex" => scalar(json!(serialize_hex(tx))),
"vin" => pick(&tx.input, name, index, |j, vin| {
resolve_vin(vin, is_coinbase && j == 0, rest)
}),
"vout" => pick(&tx.output, name, index, |_, vout| {
self.resolve_vout(vout, rest)
}),
other => Err(unknown("tx", other)),
}
}
fn resolve_vout(&self, vout: &TxOut, steps: &[Step]) -> Result<Value> {
if steps.is_empty() {
let mut obj = Map::with_capacity(VOUT_FIELDS.len());
for &name in VOUT_FIELDS {
obj.insert(
name.into(),
self.vout_field(vout, name, None, &[])
.expect("known vout field"),
);
}
return Ok(Value::Object(obj));
}
let (step, rest) = pop(steps)?;
self.vout_field(vout, &step.name, step.index, rest)
}
fn vout_field(
&self,
vout: &TxOut,
name: &str,
index: Option<usize>,
rest: &[Step],
) -> Result<Value> {
let scalar = |v| scalar_leaf(v, name, index, rest);
match name {
"value" => scalar(json!(vout.value.to_sat())),
"script_pubkey" => scalar(json!(vout.script_pubkey.to_hex_string())),
"script_pubkey_asm" => scalar(json!(vout.script_pubkey.to_asm_string())),
"type" => scalar(json!(script_type(&vout.script_pubkey))),
"address" => scalar(self.address_value(&vout.script_pubkey)),
other => Err(unknown("vout", other)),
}
}
fn address_value(&self, s: &ScriptBuf) -> Value {
Address::from_script(s, self.network)
.map(|a| Value::String(a.to_string()))
.unwrap_or(Value::Null)
}
}
fn resolve_vin(vin: &TxIn, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
if steps.is_empty() {
let mut obj = Map::with_capacity(VIN_FIELDS.len());
for &name in VIN_FIELDS {
obj.insert(
name.into(),
vin_field(vin, is_coinbase, name, None, &[]).expect("known vin field"),
);
}
return Ok(Value::Object(obj));
}
let (step, rest) = pop(steps)?;
vin_field(vin, is_coinbase, &step.name, step.index, rest)
}
fn vin_field(
vin: &TxIn,
is_coinbase: bool,
name: &str,
index: Option<usize>,
rest: &[Step],
) -> Result<Value> {
let scalar = |v| scalar_leaf(v, name, index, rest);
match name {
"prev_txid" => scalar(json!(vin.previous_output.txid.to_string())),
"prev_vout" => scalar(json!(vin.previous_output.vout)),
"sequence" => scalar(json!(vin.sequence.0)),
"script_sig" => scalar(json!(vin.script_sig.to_hex_string())),
"script_sig_asm" => scalar(json!(vin.script_sig.to_asm_string())),
"witness" => {
if !rest.is_empty() {
return Err(Error::Parse(
"'witness' element has no fields to drill into".into(),
));
}
let items: Vec<String> = vin
.witness
.iter()
.map(|w| w.to_lower_hex_string())
.collect();
pick(&items, name, index, |_, hex| Ok(Value::String(hex.clone())))
}
"has_witness" => scalar(json!(!vin.witness.is_empty())),
"is_rbf" => scalar(json!(vin.sequence.is_rbf())),
"coinbase" => scalar(json!(is_coinbase)),
other => Err(unknown("vin", other)),
}
}
fn pick<T>(
items: &[T],
name: &str,
index: Option<usize>,
mut resolve: impl FnMut(usize, &T) -> Result<Value>,
) -> Result<Value> {
match index {
Some(i) => {
let item = items
.get(i)
.ok_or_else(|| out_of_range(name, i, items.len()))?;
resolve(i, item)
}
None => Ok(Value::Array(
items
.iter()
.enumerate()
.map(|(i, item)| resolve(i, item))
.collect::<Result<_>>()?,
)),
}
}
fn pop(steps: &[Step]) -> Result<(&Step, &[Step])> {
steps
.split_first()
.ok_or_else(|| Error::Parse("empty path segment".into()))
}
fn scalar_leaf(v: Value, name: &str, index: Option<usize>, rest: &[Step]) -> Result<Value> {
if index.is_some() {
return Err(Error::Parse(format!("'{name}' is not an array")));
}
if !rest.is_empty() {
return Err(Error::Parse(format!(
"'{name}' has no fields to drill into"
)));
}
Ok(v)
}
fn out_of_range(name: &str, i: usize, len: usize) -> Error {
Error::Parse(format!("{name}.{i} out of range (len {len})"))
}
fn unknown(level: &str, name: &str) -> Error {
Error::Parse(format!(
"unknown {level} field '{name}' (run `blk --help` for the list)"
))
}
fn tx_has_witness(tx: &Transaction) -> bool {
tx.input.iter().any(|i| !i.witness.is_empty())
}
fn tx_is_rbf(tx: &Transaction) -> bool {
tx.input.iter().any(|i| i.sequence.is_rbf())
}
fn tx_total_out(tx: &Transaction) -> u64 {
tx.output.iter().map(|o| o.value.to_sat()).sum()
}
fn subsidy_sats(height: u32) -> u64 {
let halvings = height / 210_000;
if halvings >= 64 {
0
} else {
(50 * 100_000_000u64) >> halvings
}
}
fn script_type(s: &ScriptBuf) -> &'static str {
if s.is_p2pkh() {
"p2pkh"
} else if s.is_p2sh() {
"p2sh"
} else if s.is_p2wpkh() {
"p2wpkh"
} else if s.is_p2wsh() {
"p2wsh"
} else if s.is_p2tr() {
"p2tr"
} else if s.is_op_return() {
"op_return"
} else if s.is_p2pk() {
"p2pk"
} else {
"unknown"
}
}
+61
View File
@@ -0,0 +1,61 @@
use brk_error::Result;
use serde_json::{Map, Value};
use crate::{fields::Ctx, mode::Mode, path::Path};
pub struct Formatter {
mode: Mode,
fields: Vec<Path>,
}
impl Formatter {
pub fn new(mode: Mode, fields: Vec<Path>) -> Self {
Self { mode, fields }
}
pub fn format(&self, ctx: &Ctx) -> Result<String> {
match self.mode {
Mode::Bare => self.bare(ctx, false),
Mode::Tsv => self.tsv(ctx),
Mode::Json => Ok(serde_json::to_string(&self.object(ctx)?)?),
Mode::Pretty if self.fields.len() == 1 => self.bare(ctx, true),
Mode::Pretty => Ok(serde_json::to_string_pretty(&self.object(ctx)?)?),
}
}
fn bare(&self, ctx: &Ctx, pretty: bool) -> Result<String> {
Ok(match ctx.resolve(&self.fields[0])? {
Value::String(s) => s,
other if pretty => serde_json::to_string_pretty(&other)?,
other => other.to_string(),
})
}
fn tsv(&self, ctx: &Ctx) -> Result<String> {
let mut row = String::new();
for (i, path) in self.fields.iter().enumerate() {
if i > 0 {
row.push('\t');
}
for c in ctx.resolve_str(path)?.chars() {
row.push(if matches!(c, '\t' | '\n' | '\r') {
' '
} else {
c
});
}
}
Ok(row)
}
fn object(&self, ctx: &Ctx) -> Result<Value> {
if self.fields.is_empty() {
return Ok(ctx.full());
}
let mut obj = Map::with_capacity(self.fields.len());
for path in &self.fields {
obj.insert(path.raw.clone(), ctx.resolve(path)?);
}
Ok(Value::Object(obj))
}
}
+58
View File
@@ -0,0 +1,58 @@
mod args;
mod fields;
mod formatter;
mod mode;
mod path;
mod selector;
mod usage;
use std::process::ExitCode;
use brk_error::Result;
use brk_reader::Reader;
use args::Args;
use fields::Ctx;
use formatter::Formatter;
use mode::Mode;
use selector::Selector;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("blk: {e}");
ExitCode::from(1)
}
}
}
fn run() -> Result<()> {
let raw: Vec<String> = std::env::args().skip(1).collect();
if raw.is_empty() || raw.iter().any(|a| matches!(a.as_str(), "-h" | "--help")) {
usage::print();
return Ok(());
}
let args = Args::parse(raw)?;
let client = args.rpc()?;
let (start, end) = Selector::parse(&args.selector, &client)?;
let network = client.get_network()?;
let mode = Mode::pick(args.pretty, args.compact, args.paths.len())?;
let reader = Reader::new(args.blocks_dir(), &client);
let formatter = Formatter::new(mode, args.paths);
let parser_threads = (std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2)
/ 2)
.max(1);
for block in reader.range_with(start, end, parser_threads)?.iter() {
let block = block?;
let line = formatter.format(&Ctx::new(&block, network))?;
if !line.is_empty() {
println!("{line}");
}
}
Ok(())
}
+35
View File
@@ -0,0 +1,35 @@
use brk_error::{Error, Result};
#[derive(Clone, Copy)]
pub enum Mode {
Bare,
Tsv,
Json,
Pretty,
}
impl Mode {
pub fn pick(pretty: bool, compact: bool, n_fields: usize) -> Result<Self> {
if pretty && compact {
return Err(Error::Parse(
"--pretty and --compact are mutually exclusive".into(),
));
}
if compact && n_fields == 0 {
return Err(Error::Parse(
"--compact requires at least one field".into(),
));
}
Ok(if pretty {
Self::Pretty
} else if n_fields == 0 {
Self::Json
} else if n_fields == 1 {
Self::Bare
} else if compact {
Self::Tsv
} else {
Self::Json
})
}
}
+40
View File
@@ -0,0 +1,40 @@
use brk_error::{Error, Result};
pub struct Step {
pub name: String,
pub index: Option<usize>,
}
pub struct Path {
pub raw: String,
pub steps: Vec<Step>,
}
impl Path {
pub fn parse(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.split('.').collect();
let mut steps = Vec::new();
let mut i = 0;
while i < parts.len() {
let name = parts[i];
if name.is_empty() {
return Err(Error::Parse(format!("bad path '{s}': empty segment")));
}
if name.parse::<usize>().is_ok() {
return Err(Error::Parse(format!(
"bad path '{s}': '{name}' must follow a field name"
)));
}
let index = parts.get(i + 1).and_then(|p| p.parse::<usize>().ok());
steps.push(Step {
name: name.to_string(),
index,
});
i += if index.is_some() { 2 } else { 1 };
}
Ok(Self {
raw: s.to_string(),
steps,
})
}
}
+44
View File
@@ -0,0 +1,44 @@
use brk_error::{Error, Result};
use brk_rpc::Client;
use brk_types::{CheckedSub, Height};
pub struct Selector;
impl Selector {
pub fn parse(s: &str, client: &Client) -> Result<(Height, Height)> {
let (a, b) = s.split_once("..").unwrap_or((s, s));
let needs_tip = |p: &str| p == "tip" || p.starts_with("tip-");
let tip = if needs_tip(a) || needs_tip(b) {
Some(client.get_last_height()?)
} else {
None
};
let start = Self::endpoint(a, tip)?;
let end = Self::endpoint(b, tip)?;
if end < start {
return Err(Error::Parse(format!(
"range end {end} before start {start}"
)));
}
Ok((start, end))
}
fn endpoint(s: &str, tip: Option<Height>) -> Result<Height> {
if s == "tip" {
return Ok(tip.expect("tip pre-resolved when input contains 'tip'"));
}
if let Some(rest) = s.strip_prefix("tip-") {
let n: u32 = rest
.parse()
.map_err(|_| Error::Parse(format!("bad tip offset: {s}")))?;
let tip = tip.expect("tip pre-resolved when input contains 'tip'");
return tip
.checked_sub(n)
.ok_or_else(|| Error::Parse(format!("tip-{n} underflows genesis")));
}
let n: u32 = s
.parse()
.map_err(|_| Error::Parse(format!("bad height: {s}")))?;
Ok(Height::new(n))
}
}
+190
View File
@@ -0,0 +1,190 @@
use owo_colors::{OwoColorize, Stream};
const SEL_W: usize = 5; // longest selector token: "tip-N"
const LABEL_W: usize = 28; // longest label across OUTPUT/OPTIONS/EXAMPLES (= example cmd "blk 800000 tx.0.vout.0.value")
const FLAG_W: usize = 15; // longest flag: "--rpccookiefile"
const PH_W: usize = LABEL_W - FLAG_W - 1; // placeholder column width so flag+ph total = LABEL_W
const GAP: usize = 4;
pub fn print() {
println!("{} - inspect a Bitcoin Core block", bold("blk"));
println!();
section("USAGE");
println!(
" blk {} [{} ...] [OPTIONS]",
dim("<selector>"),
dim("<field>")
);
println!(
" {}",
dim("no fields = full block as JSON (analog of `bitcoin-cli getblock <hash> 2`)")
);
println!();
section("SELECTOR");
sel("<n>", "single height (e.g. 800000)");
sel("tip", "current chain tip");
sel("tip-N", "tip minus N");
sel("a..b", "inclusive range, endpoints can be height/tip/tip-N");
println!();
section("FIELDS");
println!(
" {}",
dim("dotted paths drill into nested data, omit an index for arrays")
);
println!();
group("block");
fields(&[
"height, hash, time, version, version_hex, bits, nonce,",
"prev, merkle, difficulty, txs, n_inputs, n_outputs,",
"witness_txs, size, strippedsize, weight, subsidy,",
"coinbase, coinbase_hex, header_hex, hex",
]);
println!();
group_note("tx.i", "omit i for all txs");
fields(&[
"txid, wtxid, version, locktime, size, base_size, vsize,",
"weight, inputs, outputs, is_coinbase, has_witness, is_rbf,",
"total_out, hex",
]);
println!();
group_note("tx.i.vin.j", "omit j for all inputs");
fields(&[
"prev_txid, prev_vout, sequence, script_sig, script_sig_asm,",
"witness, has_witness, is_rbf, coinbase",
]);
println!();
group_note("tx.i.vout.j", "omit j for all outputs");
fields(&["value, script_pubkey, script_pubkey_asm, type, address"]);
println!();
println!(
" {}",
dim("Naked tx / tx.i / vin / vout returns the whole sub-object as JSON.")
);
println!();
section("OUTPUT");
out("no fields", "full block JSON object, one per line (NDJSON)");
out("1 field", "bare value, one per line");
out("2+ fields", "JSON object, one per line (NDJSON)");
out("-p, --pretty", "pretty JSON object instead");
out(
"-c, --compact",
"tab-separated values, no field names (TSV)",
);
println!();
section("OPTIONS");
opt(
"--bitcoindir",
"<PATH>",
"Bitcoin directory",
Some("[OS default]"),
);
opt(
"--blocksdir",
"<PATH>",
"Blocks directory",
Some("[<bitcoindir>/blocks]"),
);
opt("--rpcconnect", "<IP>", "RPC host", Some("[localhost]"));
opt("--rpcport", "<PORT>", "RPC port", Some("[8332]"));
opt(
"--rpccookiefile",
"<PATH>",
"RPC cookie file",
Some("[<bitcoindir>/.cookie]"),
);
opt("--rpcuser", "<USERNAME>", "RPC username", None);
opt("--rpcpassword", "<PASSWORD>", "RPC password", None);
println!();
section("EXAMPLES");
ex("blk 800000", "full block as JSON");
ex("blk 800000 hash", "bare hash");
ex("blk 800000 height hash time", "one compact JSON line");
ex("blk 800000 tx.0.txid", "coinbase txid");
ex("blk 800000 tx.txid", "all txids in block (array)");
ex("blk 800000 tx.0.vout.0.value", "coinbase output 0 sats");
ex("blk 800000 tx.0.vout.value", "all output sats for tx 0");
ex("blk 800000 tx.vout.value", "array of arrays (per tx)");
ex("blk 0..2 hash tx.0.txid", "3 NDJSON lines");
ex("blk tip tx.0", "whole coinbase tx as JSON");
}
fn section(name: &str) {
println!("{}", bold(&format!("{name}:")));
}
fn group(name: &str) {
println!(" {}", bold(&format!("{name}:")));
}
fn group_note(name: &str, note: &str) {
println!(
" {} {}",
bold(&format!("{name}:")),
dim(&format!("({note})"))
);
}
fn fields(lines: &[&str]) {
for line in lines {
println!(" {line}");
}
}
fn pad(s: &str, width: usize) -> String {
" ".repeat(width.saturating_sub(s.len()))
}
fn sel(token: &str, desc: &str) {
println!(
" {}{}{}{desc}",
dim(token),
pad(token, SEL_W),
" ".repeat(GAP),
);
}
fn out(label: &str, desc: &str) {
println!(
" {label}{}{}{desc}",
pad(label, LABEL_W),
" ".repeat(GAP)
);
}
fn opt(flag: &str, ph: &str, desc: &str, default: Option<&str>) {
let head = format!(
" {flag}{} {}{}{}",
pad(flag, FLAG_W),
dim(ph),
pad(ph, PH_W),
" ".repeat(GAP),
);
match default {
Some(d) => println!("{head}{desc} {}", dim(d)),
None => println!("{head}{desc}"),
}
}
fn ex(cmd: &str, note: &str) {
println!(
" {cmd}{}{}{}",
pad(cmd, LABEL_W),
" ".repeat(GAP),
dim(&format!("# {note}"))
);
}
fn bold(s: &str) -> String {
s.if_supports_color(Stream::Stdout, |t| t.bold()).to_string()
}
fn dim(s: &str) -> String {
s.if_supports_color(Stream::Stdout, |t| t.bright_black())
.to_string()
}
+2 -2
View File
@@ -8,5 +8,5 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
libmimalloc-sys = { version = "0.1.47", features = ["extended"] }
mimalloc = { version = "0.1.50" }
libmimalloc-sys = { version = "0.1.49", features = ["extended"] }
mimalloc = { version = "0.1.52" }
+1 -1
View File
@@ -12,6 +12,6 @@ brk_cohort = { workspace = true }
brk_query = { workspace = true }
brk_types = { workspace = true }
indexmap = { workspace = true }
oas3 = "0.21"
oas3 = "0.22"
serde = { workspace = true }
serde_json = { workspace = true }
+10 -7
View File
@@ -6,11 +6,11 @@
use std::collections::BTreeMap;
use brk_cohort::{
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, EPOCH_NAMES, LOSS_NAMES, OVER_AGE_NAMES,
OVER_AMOUNT_NAMES, PROFIT_NAMES, PROFITABILITY_RANGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES,
UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES,
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, ENTRY_NAMES, EPOCH_NAMES, LOSS_NAMES,
OVER_AGE_NAMES, OVER_AMOUNT_NAMES, PROFIT_NAMES, PROFITABILITY_RANGE_NAMES,
SPENDABLE_TYPE_NAMES, TERM_NAMES, UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES,
};
use brk_types::{Index, PoolSlug, pools};
use brk_types::{Index, pools};
use serde::Serialize;
use serde_json::Value;
@@ -20,7 +20,7 @@ use crate::{VERSION, to_camel_case};
pub struct ClientConstants {
pub version: String,
pub indexes: Vec<&'static str>,
pub pool_map: BTreeMap<PoolSlug, &'static str>,
pub pool_map: BTreeMap<String, &'static str>,
}
impl ClientConstants {
@@ -32,8 +32,10 @@ impl ClientConstants {
let pools = pools();
let mut sorted_pools: Vec<_> = pools.iter().collect();
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();
let pool_map: BTreeMap<String, &'static str> = sorted_pools
.iter()
.map(|p| (p.slug().to_string(), p.name))
.collect();
Self {
version: format!("v{}", VERSION),
@@ -57,6 +59,7 @@ impl CohortConstants {
("TERM_NAMES", to_value(&TERM_NAMES)),
("EPOCH_NAMES", to_value(&EPOCH_NAMES)),
("CLASS_NAMES", to_value(&CLASS_NAMES)),
("ENTRY_NAMES", to_value(&ENTRY_NAMES)),
("SPENDABLE_TYPE_NAMES", to_value(&SPENDABLE_TYPE_NAMES)),
("AGE_RANGE_NAMES", to_value(&AGE_RANGE_NAMES)),
("UNDER_AGE_NAMES", to_value(&UNDER_AGE_NAMES)),
@@ -4,7 +4,7 @@ use std::fmt::Write;
use crate::{
Endpoint, Parameter,
generators::{normalize_return_type, write_description},
generators::{javascript::types::jsdoc_normalize, normalize_return_type, write_description},
to_camel_case,
};
@@ -14,127 +14,250 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
if !endpoint.should_generate() {
continue;
}
match endpoint.method.as_str() {
"GET" => generate_get_method(output, endpoint),
"POST" => generate_post_method(output, endpoint),
_ => continue,
}
}
}
let method_name = endpoint_to_method_name(endpoint);
let base_return_type =
normalize_return_type(endpoint.response_type.as_deref().unwrap_or("*"));
let return_type = if endpoint.supports_csv {
format!("{} | string", base_return_type)
fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
for param in &endpoint.path_params {
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(output, " * @param {{{}}} {}{}", ty, param.name, desc).unwrap();
}
for param in &endpoint.query_params {
let optional = if param.required { "" } else { "=" };
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
let ident = sanitize_ident(&param.name);
let name_decl = if param.required {
ident
} else {
base_return_type
format!("[{}]", ident)
};
writeln!(
output,
" * @param {{{}{}}} {}{}",
ty, optional, name_decl, desc
)
.unwrap();
}
writeln!(
output,
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void, cache?: boolean }}}} [options]",
return_type
)
.unwrap();
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
writeln!(output, " */").unwrap();
writeln!(output, " /**").unwrap();
if let Some(summary) = &endpoint.summary {
writeln!(output, " * {}", summary).unwrap();
}
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " *").unwrap();
write_description(output, desc, " * ", " *");
}
let params = build_method_params(endpoint);
let params_with_opts = if params.is_empty() {
"{ signal, onValue, cache } = {}".to_string()
} else {
format!("{}, {{ signal, onValue, cache }} = {{}}", params)
};
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
// Add endpoint path
let path = build_path_template(&endpoint.path, &endpoint.path_params);
let fetch_call: String = if endpoint.returns_binary() {
"this.getBytes(path, { signal, onValue, cache })".to_string()
} else if endpoint.returns_json() {
"this.getJson(path, { signal, onValue, cache })".to_string()
} else if endpoint.response_kind.text_is_numeric() {
"Number(await this.getText(path, { signal, cache, onValue: onValue ? (v) => onValue(Number(v)) : undefined }))".to_string()
} else {
"this.getText(path, { signal, onValue, cache })".to_string()
};
write_path_assignment(output, endpoint, &path);
if endpoint.supports_csv {
writeln!(
output,
" if (format === 'csv') return this.getText(path, {{ signal, onValue, cache }});"
)
.unwrap();
}
writeln!(output, " return {};", fetch_call).unwrap();
writeln!(output, " }}\n").unwrap();
}
fn generate_post_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
for param in &endpoint.path_params {
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(output, " * @param {{{}}} {}{}", ty, param.name, desc).unwrap();
}
for param in &endpoint.query_params {
let optional = if param.required { "" } else { "=" };
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(
output,
" * @param {{{}{}}} [{}]{}",
ty, optional, param.name, desc
)
.unwrap();
}
if let Some(body) = &endpoint.request_body {
let optional = if body.required { "" } else { "=" };
let ty = jsdoc_normalize(&body.body_type);
writeln!(
output,
" * @param {{{}{}}} body - Request body",
ty, optional
)
.unwrap();
}
writeln!(
output,
" * @param {{{{ signal?: AbortSignal }}}} [options]"
)
.unwrap();
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
writeln!(output, " */").unwrap();
let mut params = build_method_params(endpoint);
if endpoint.request_body.is_some() {
if !params.is_empty() {
params.push_str(", ");
}
params.push_str("body");
}
let params_with_opts = if params.is_empty() {
"{ signal } = {}".to_string()
} else {
format!("{}, {{ signal }} = {{}}", params)
};
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
let path = build_path_template(&endpoint.path, &endpoint.path_params);
let body_arg = if endpoint.request_body.is_some() {
"body"
} else {
"''"
};
let fetch_call: String = if endpoint.returns_binary() {
format!("this.postBytes(path, {}, {{ signal }})", body_arg)
} else if endpoint.returns_json() {
format!("this.postJson(path, {}, {{ signal }})", body_arg)
} else if endpoint.response_kind.text_is_numeric() {
format!(
"Number(await this.postText(path, {}, {{ signal }}))",
body_arg
)
} else {
format!("this.postText(path, {}, {{ signal }})", body_arg)
};
write_path_assignment(output, endpoint, &path);
writeln!(output, " return {};", fetch_call).unwrap();
writeln!(output, " }}\n").unwrap();
}
fn build_return_type(endpoint: &Endpoint) -> String {
let base = if endpoint.returns_binary() {
"Uint8Array".to_string()
} else {
jsdoc_normalize(&normalize_return_type(
endpoint.schema_name().unwrap_or("*"),
))
};
if endpoint.supports_csv {
format!("{} | string", base)
} else {
base
}
}
fn write_method_doc(output: &mut String, endpoint: &Endpoint) {
writeln!(output, " /**").unwrap();
if let Some(summary) = &endpoint.summary {
writeln!(output, " * {}", summary).unwrap();
}
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " *").unwrap();
writeln!(
output,
" * Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
write_description(output, desc, " * ", " *");
}
writeln!(output, " *").unwrap();
writeln!(
output,
" * Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() {
writeln!(output, " *").unwrap();
}
let has_body_param = endpoint.method == "POST" && endpoint.request_body.is_some();
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() || has_body_param {
writeln!(output, " *").unwrap();
}
}
for param in &endpoint.path_params {
let desc = format_param_desc(param.description.as_deref());
writeln!(
output,
" * @param {{{}}} {}{}",
param.param_type, param.name, desc
)
.unwrap();
}
fn write_path_assignment(output: &mut String, endpoint: &Endpoint, path: &str) {
if endpoint.query_params.is_empty() {
writeln!(output, " const path = `{}`;", path).unwrap();
} else {
writeln!(output, " const params = new URLSearchParams();").unwrap();
for param in &endpoint.query_params {
let optional = if param.required { "" } else { "=" };
let desc = format_param_desc(param.description.as_deref());
writeln!(
output,
" * @param {{{}{}}} [{}]{}",
param.param_type, optional, param.name, desc
)
.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);
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, " const path = `{}`;", path).unwrap();
} else {
writeln!(output, " const params = new URLSearchParams();").unwrap();
for param in &endpoint.query_params {
let ident = sanitize_ident(&param.name);
let ident = sanitize_ident(&param.name);
let is_array = param.param_type.ends_with("[]");
if is_array {
if param.required {
writeln!(
output,
" params.set('{}', String({}));",
param.name, ident
" for (const _v of {}) params.append('{}', String(_v));",
ident, param.name
)
.unwrap();
} else {
writeln!(
output,
" if ({} !== undefined) params.set('{}', String({}));",
ident, param.name, ident
" if ({}) for (const _v of {}) params.append('{}', String(_v));",
ident, ident, param.name
)
.unwrap();
}
} else if param.required {
writeln!(
output,
" params.set('{}', String({}));",
param.name, ident
)
.unwrap();
} else {
writeln!(
output,
" if ({} !== undefined) params.set('{}', String({}));",
ident, param.name, ident
)
.unwrap();
}
writeln!(output, " const query = params.toString();").unwrap();
writeln!(
output,
" const path = `{}${{query ? '?' + query : ''}}`;",
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();
writeln!(output, " const query = params.toString();").unwrap();
writeln!(
output,
" const path = `{}${{query ? '?' + query : ''}}`;",
path
)
.unwrap();
}
}
@@ -16,12 +16,16 @@ pub fn generate_base_client(output: &mut String) {
* @typedef {{Object}} BrkClientOptions
* @property {{string}} baseUrl - Base URL for the API
* @property {{number}} [timeout] - Request timeout in milliseconds
* @property {{string|boolean}} [cache] - Enable browser cache with default name (true), custom name (string), or disable (false). No effect in Node.js. Default: true
* @property {{string|boolean}} [browserCache] - Enable browser Cache API with default name (true), custom name (string), or disable (false). No effect in Node.js. Default: true
* @property {{number|boolean}} [memCache] - In-memory parsed-response cache size (LRU). true/undefined → 1000, false/0 → disabled. Lets 304 responses skip the JSON parse entirely. Default: 1000
*/
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
const _defaultCacheName = '__BRK_CLIENT__';
const _defaultBrowserCacheName = '__BRK_CLIENT__';
const _DEFAULT_MEM_CACHE_SIZE = 1000;
/** @template T @typedef {{{{ etag: string | null, value: T }}}} _MemEntry */
/** @param {{*}} v */
const _addCamelGetters = (v) => {{
if (Array.isArray(v)) {{ v.forEach(_addCamelGetters); return v; }}
@@ -38,15 +42,21 @@ const _addCamelGetters = (v) => {{
}};
/**
* @param {{string|boolean|undefined}} cache
* @param {{string|boolean|undefined}} option
* @returns {{Promise<Cache | null>}}
*/
const _openCache = (cache) => {{
if (!_isBrowser || cache === false) return Promise.resolve(null);
const name = typeof cache === 'string' ? cache : _defaultCacheName;
const _openBrowserCache = (option) => {{
if (!_isBrowser || option === false) return Promise.resolve(null);
const name = typeof option === 'string' ? option : _defaultBrowserCacheName;
return caches.open(name).catch(() => null);
}};
/**
* @param {{string}} url
* @returns {{URL}}
*/
const _parseBaseUrl = (url) => new URL(url, typeof location === 'undefined' ? undefined : location.href);
/**
* Custom error class for BRK client errors
*/
@@ -198,7 +208,6 @@ function _wrapSeriesData(raw) {{
* @property {{number}} version - Version of the series data
* @property {{Index}} index - The index type used for this query
* @property {{string}} type - Value type (e.g. "f32", "u64", "Sats")
* @property {{number}} total - Total number of data points
* @property {{number}} start - Start index (inclusive)
* @property {{number}} end - End index (exclusive)
* @property {{string}} stamp - ISO 8601 timestamp of when the response was generated
@@ -234,8 +243,10 @@ function _wrapSeriesData(raw) {{
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch all data
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{() => Promise<number>}} len - Get total number of data points
* @property {{() => Promise<Version>}} version - Get the current version of the series
* @property {{Thenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
@@ -248,8 +259,10 @@ function _wrapSeriesData(raw) {{
* @property {{(n: number) => DateRangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => DateRangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => DateSkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch all data
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{() => Promise<number>}} len - Get total number of data points
* @property {{() => Promise<Version>}} version - Get the current version of the series
* @property {{DateThenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
@@ -257,39 +270,39 @@ function _wrapSeriesData(raw) {{
/** @typedef {{SeriesEndpoint<any>}} AnySeriesEndpoint */
/** @template T @typedef {{Object}} SingleItemBuilder
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the item
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSingleItemBuilder
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the item
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} SkippedBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSkippedBuilder
* @property {{(n: number) => DateRangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} RangeBuilder
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the range
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateRangeBuilder
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the range
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
@@ -308,7 +321,7 @@ function _wrapSeriesData(raw) {{
/**
* Create a series endpoint builder with typestate pattern.
* @template T
* @param {{BrkClientBase}} client
* @param {{BrkClient}} client
* @param {{string}} name - The series vec name
* @param {{Index}} index - The index name
* @returns {{DateSeriesEndpoint<T>}}
@@ -337,7 +350,7 @@ function _endpoint(client, name, index) {{
* @returns {{DateRangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(start, end), onUpdate); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, end), onValue); }},
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -347,7 +360,7 @@ function _endpoint(client, name, index) {{
* @returns {{DateSingleItemBuilder<T>}}
*/
const singleItemBuilder = (idx) => ({{
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(idx, idx + 1), onUpdate); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(idx, idx + 1), onValue); }},
fetchCsv() {{ return client.getText(buildPath(idx, idx + 1, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -358,7 +371,7 @@ function _endpoint(client, name, index) {{
*/
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(start, undefined), onUpdate); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, undefined), onValue); }},
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -374,8 +387,10 @@ function _endpoint(client, name, index) {{
first(n) {{ return rangeBuilder(undefined, n); }},
last(n) {{ return n === 0 ? rangeBuilder(undefined, 0) : rangeBuilder(-n, undefined); }},
skip(n) {{ return skippedBuilder(n); }},
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(), onUpdate); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(), onValue); }},
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
len() {{ return client.getSeriesLen(name, index); }},
version() {{ return client.getSeriesVersion(name, index); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
get path() {{ return p; }},
}};
@@ -394,89 +409,163 @@ class BrkClientBase {{
const isString = typeof options === 'string';
const rawUrl = isString ? options : options.baseUrl;
this.baseUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl;
const url = _parseBaseUrl(this.baseUrl);
this.url = url.href.endsWith('/') ? url.href.slice(0, -1) : url.href;
this.domain = url.hostname;
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
/** @type {{Promise<Cache | null>}} */
this._cachePromise = _openCache(isString ? undefined : options.cache);
this._browserCachePromise = _openBrowserCache(isString ? undefined : options.browserCache);
/** @type {{Cache | null}} */
this._cache = null;
this._cachePromise.then(c => this._cache = c);
this._browserCache = null;
this._browserCachePromise.then(c => this._browserCache = c);
const memOpt = isString ? undefined : options.memCache;
this._memCacheMax = memOpt === false || memOpt === 0
? 0
: (typeof memOpt === 'number' ? memOpt : _DEFAULT_MEM_CACHE_SIZE);
/** @type {{Map<string, _MemEntry<unknown>>}} */
this._memCache = new Map();
}}
/**
* @template T
* @param {{string}} key
* @returns {{_MemEntry<T> | undefined}}
*/
_memGet(key) {{
if (!this._memCacheMax) return undefined;
const hit = this._memCache.get(key);
if (!hit) return undefined;
this._memCache.delete(key);
this._memCache.set(key, hit);
return /** @type {{_MemEntry<T>}} */ (hit);
}}
/**
* @param {{string}} key
* @param {{string | null}} etag
* @param {{unknown}} value
*/
_memSet(key, etag, value) {{
if (!this._memCacheMax) return;
if (this._memCache.has(key)) this._memCache.delete(key);
else if (this._memCache.size >= this._memCacheMax) {{
const oldest = this._memCache.keys().next().value;
if (oldest !== undefined) this._memCache.delete(oldest);
}}
this._memCache.set(key, {{ etag, value }});
}}
/**
* @param {{string}} path
* @param {{{{ signal?: AbortSignal }}}} [options]
* @param {{{{ signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<Response>}}
*/
async get(path, {{ signal }} = {{}}) {{
async get(path, {{ signal, cache = true }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const signals = [AbortSignal.timeout(this.timeout)];
if (signal) signals.push(signal);
const res = await fetch(url, {{ signal: AbortSignal.any(signals) }});
/** @type {{RequestInit}} */
const init = {{ signal: AbortSignal.any(signals) }};
if (!cache) init.cache = 'no-store';
const res = await fetch(url, init);
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.
* Shared implementation backing `getJson` and `getText`.
* Make a GET request with layered caching.
*
* Contract:
* - The returned Promise resolves with the **freshest** value (post-revalidation).
* - `onValue` fires once with the freshest value, or twice if a stale snapshot
* could be shown first (stale-while-revalidate). On a 304 there is no second fire.
*
* Layers:
* - L1 (memCache): in-memory parsed values keyed by URL+ETag. Lets 304s skip the parse entirely.
* - L2 (browserCache): Cache API, survives reload and feeds onValue fast on cold start.
*
* @template T
* @param {{string}} path
* @param {{(res: Response) => Promise<T>}} parse - Response body reader
* @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options]
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<T>}}
*/
async _getCached(path, parse, {{ onUpdate, signal }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const cache = this._cache ?? await this._cachePromise;
let resolved = false;
/** @type {{Response | null}} */
let cachedRes = null;
// Race cache vs network - first to resolve calls onUpdate
const cachePromise = cache?.match(url).then(async (res) => {{
cachedRes = res ?? null;
if (!res) return null;
async _getCached(path, parse, {{ onValue, signal, cache = true }} = {{}}) {{
if (!cache) {{
const res = await this.get(path, {{ signal, cache }});
const value = await parse(res);
if (!resolved && onUpdate) {{
resolved = true;
onUpdate(value);
}}
if (onValue) onValue(value);
return value;
}});
}}
const networkPromise = this.get(path, {{ signal }}).then(async (res) => {{
const cloned = res.clone();
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(value);
const url = `${{this.baseUrl}}${{path}}`;
/** @type {{_MemEntry<T> | undefined}} */
const memHit = this._memGet(url);
const browserCache = this._browserCache;
// L1 fast path: deliver from memCache, revalidate via network.
// ETag match → zero parse, zero clone, zero cache write, no second onValue fire.
if (memHit) {{
if (onValue) onValue(memHit.value);
try {{
const res = await this.get(path, {{ signal }});
const netEtag = res.headers.get('ETag');
if (netEtag && netEtag === memHit.etag) return memHit.value;
const cloned = browserCache ? res.clone() : null;
const value = await parse(res);
this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cacheStore = browserCache;
_runIdle(() => cacheStore.put(url, cloned));
}}
return value;
}} catch {{
return memHit.value;
}}
resolved = true;
if (onUpdate) onUpdate(value);
if (cache) _runIdle(() => cache.put(url, cloned));
return value;
}});
}}
// L1 miss: race browserCache (stale snapshot) vs network (fresh).
let networkSettled = false;
const stalePromise = onValue && browserCache
? browserCache.match(url).then(async (res) => {{
if (!res || networkSettled) return null;
const value = await parse(res);
if (networkSettled) return value;
this._memSet(url, res.headers.get('ETag'), value);
onValue(value);
return value;
}}).catch(() => null)
: null;
try {{
return await networkPromise;
const res = await this.get(path, {{ signal }});
networkSettled = true;
const netEtag = res.headers.get('ETag');
// Stale won and populated memCache with matching ETag → reuse, skip parse + second onValue.
const populated = /** @type {{_MemEntry<T> | undefined}} */ (this._memGet(url));
if (populated && netEtag && netEtag === populated.etag) return populated.value;
const cloned = browserCache ? res.clone() : null;
const value = await parse(res);
this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cacheStore = browserCache;
_runIdle(() => cacheStore.put(url, cloned));
}}
return value;
}} catch (e) {{
// Network failed - wait for cache
const cachedValue = await cachePromise?.catch(() => null);
if (cachedValue != null) return cachedValue;
const stale = await stalePromise;
if (stale != null) return stale;
throw e;
}}
}}
/**
* Make a GET request expecting a JSON response. Cached and supports `onUpdate`.
* Make a GET request expecting a JSON response. Cached and supports `onValue`.
* @template T
* @param {{string}} path
* @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options]
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<T>}}
*/
getJson(path, options) {{
@@ -485,25 +574,97 @@ class BrkClientBase {{
/**
* Make a GET request expecting a text response (text/plain, text/csv, ...).
* Cached and supports `onUpdate`, same as `getJson`.
* Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path
* @param {{{{ onUpdate?: (value: string) => void, signal?: AbortSignal }}}} [options]
* @param {{{{ onValue?: (value: string) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<string>}}
*/
getText(path, options) {{
return this._getCached(path, (res) => res.text(), options);
}}
/**
* Make a GET request expecting binary data (application/octet-stream).
* Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path
* @param {{{{ onValue?: (value: Uint8Array) => void, signal?: AbortSignal, cache?: boolean }}}} [options]
* @returns {{Promise<Uint8Array>}}
*/
getBytes(path, options) {{
return this._getCached(path, async (res) => new Uint8Array(await res.arrayBuffer()), options);
}}
/**
* Make a POST request with a string body.
*
* POST responses are uncached and never invoke `onValue` — every call hits
* the network with the same body and returns the upstream response.
*
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<Response>}}
*/
async post(path, body, {{ signal }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const signals = [AbortSignal.timeout(this.timeout)];
if (signal) signals.push(signal);
const res = await fetch(url, {{
method: 'POST',
body,
signal: AbortSignal.any(signals),
}});
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
return res;
}}
/**
* Make a POST request expecting a JSON response.
* @template T
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<T>}}
*/
async postJson(path, body, options) {{
const res = await this.post(path, body, options);
return _addCamelGetters(await res.json());
}}
/**
* Make a POST request expecting a text response.
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<string>}}
*/
async postText(path, body, options) {{
const res = await this.post(path, body, options);
return res.text();
}}
/**
* Make a POST request expecting binary data (application/octet-stream).
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<Uint8Array>}}
*/
async postBytes(path, body, options) {{
const res = await this.post(path, body, options);
return new Uint8Array(await res.arrayBuffer());
}}
/**
* Fetch series data and wrap with helper methods (internal)
* @template T
* @param {{string}} path
* @param {{(value: DateSeriesData<T>) => void}} [onUpdate]
* @param {{(value: DateSeriesData<T>) => void}} [onValue]
* @returns {{Promise<DateSeriesData<T>>}}
*/
async _fetchSeriesData(path, onUpdate) {{
const wrappedOnUpdate = onUpdate ? (/** @type {{SeriesData<T>}} */ raw) => onUpdate(_wrapSeriesData(raw)) : undefined;
const raw = await this.getJson(path, {{ onUpdate: wrappedOnUpdate }});
async _fetchSeriesData(path, onValue) {{
const wrappedOnValue = onValue ? (/** @type {{SeriesData<T>}} */ raw) => onValue(_wrapSeriesData(raw)) : undefined;
const raw = await this.getJson(path, {{ onValue: wrappedOnValue }});
return _wrapSeriesData(raw);
}}
}}
@@ -626,7 +787,7 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
r#"/**
* Generic series pattern factory.
* @template T
* @param {{BrkClientBase}} client
* @param {{BrkClient}} client
* @param {{string}} name - The series vec name
* @param {{readonly Index[]}} indexes - The supported indexes
*/
@@ -679,7 +840,7 @@ function _mp(client, name, indexes) {{
// Generate thin wrapper that calls the generic factory
writeln!(
output,
"/** @template T @param {{BrkClientBase}} client @param {{string}} name @returns {{{}<T>}} */",
"/** @template T @param {{BrkClient}} client @param {{string}} name @returns {{{}<T>}} */",
pattern.name
)
.unwrap();
@@ -741,7 +902,7 @@ pub fn generate_structural_patterns(
if pattern.is_generic {
writeln!(output, " * @template T").unwrap();
}
writeln!(output, " * @param {{BrkClientBase}} client").unwrap();
writeln!(output, " * @param {{BrkClient}} client").unwrap();
writeln!(output, " * @param {{string}} acc - Accumulated series name").unwrap();
if pattern.is_templated() {
writeln!(output, " * @param {{string}} disc - Discriminator suffix").unwrap();
@@ -52,7 +52,7 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
.map(|arr| arr.iter().any(|v| v.as_str() == Some(prop_name)))
.unwrap_or(false);
let optional = if required { "" } else { "=" };
let safe_name = to_camel_case(prop_name);
let safe_name = to_camel_case(&prop_name.replace(['[', ']'], ""));
let prop_desc = prop_schema
.get("description")
.and_then(|d| d.as_str())
@@ -111,6 +111,25 @@ fn json_type_to_js(ty: &str, schema: &Value, current_type: Option<&str>) -> Stri
}
}
/// JSDoc has no `integer` keyword, only `number`. Map `integer` (and `integer[]`,
/// `Foo<integer>`, etc.) to `number` before emitting type strings to JS.
pub fn jsdoc_normalize(ty: &str) -> String {
let mut out = ty.to_string();
let mut prev = String::new();
while prev != out {
prev = out.clone();
out = out.replace("integer[]", "number[]");
out = out.replace("<integer>", "<number>");
out = out.replace("(integer)", "(number)");
out = out.replace("integer | ", "number | ");
out = out.replace(" | integer", " | number");
}
if out == "integer" {
return "number".to_string();
}
out
}
/// Convert a JSON schema to a JavaScript type string.
pub fn schema_to_js_type(schema: &Value, current_type: Option<&str>) -> String {
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
+70 -15
View File
@@ -96,13 +96,16 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
}
let method_name = endpoint_to_method_name(endpoint);
let base_return_type = normalize_return_type(
&endpoint
.response_type
.as_deref()
.map(js_type_to_python)
.unwrap_or_else(|| "str".to_string()),
);
let base_return_type = if endpoint.returns_binary() {
"bytes".to_string()
} else {
normalize_return_type(
&endpoint
.schema_name()
.map(js_type_to_python)
.unwrap_or_else(|| "str".to_string()),
)
};
let return_type = if endpoint.supports_csv {
format!("Union[{}, str]", base_return_type)
@@ -159,24 +162,58 @@ 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"
let is_post = endpoint.method == "POST";
let fetch_method = match (is_post, &endpoint.response_kind) {
(false, _) if endpoint.returns_binary() => "get",
(false, _) if endpoint.returns_json() => "get_json",
(false, _) => "get_text",
(true, _) if endpoint.returns_binary() => "post",
(true, _) if endpoint.returns_json() => "post_json",
(true, _) => "post_text",
};
let body_arg = if is_post && endpoint.request_body.is_some() {
", body"
} else {
"get_text"
""
};
let (wrap_prefix, wrap_suffix) = if endpoint.response_kind.text_is_numeric() {
("int(", ")")
} else {
("", "")
};
if endpoint.query_params.is_empty() {
if endpoint.path_params.is_empty() {
writeln!(output, " return self.{}('{}')", fetch_method, path).unwrap();
writeln!(
output,
" return {}self.{}('{}'{}){}",
wrap_prefix, fetch_method, path, body_arg, wrap_suffix
)
.unwrap();
} else {
writeln!(output, " return self.{}(f'{}')", fetch_method, path).unwrap();
writeln!(
output,
" return {}self.{}(f'{}'{}){}",
wrap_prefix, fetch_method, path, body_arg, wrap_suffix
)
.unwrap();
}
} else {
writeln!(output, " params = []").unwrap();
for param in &endpoint.query_params {
// Use safe name for Python variable, original name for API query parameter
let safe_name = escape_python_keyword(&param.name);
if param.required {
let is_array = param.param_type.ends_with("[]");
if is_array {
writeln!(
output,
" for _v in {}: params.append(f'{}={{_v}}')",
safe_name, param.name
)
.unwrap();
} else if param.required {
writeln!(
output,
" params.append(f'{}={{{}}}')",
@@ -203,9 +240,19 @@ 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.{}(path)", fetch_method).unwrap();
writeln!(
output,
" return {}self.{}(path{}){}",
wrap_prefix, fetch_method, body_arg, wrap_suffix
)
.unwrap();
} else {
writeln!(output, " return self.{}(path)", fetch_method).unwrap();
writeln!(
output,
" return {}self.{}(path{}){}",
wrap_prefix, fetch_method, body_arg, wrap_suffix
)
.unwrap();
}
}
@@ -240,6 +287,14 @@ fn build_method_params(endpoint: &Endpoint) -> String {
params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
}
}
if let Some(body) = &endpoint.request_body {
let py_type = js_type_to_python(&body.body_type);
if body.required {
params.push(format!(", body: {}", py_type));
} else {
params.push(format!(", body: Optional[{}] = None", py_type));
}
}
params.join("")
}
@@ -99,6 +99,28 @@ class BrkClientBase:
"""Make a GET request and return text."""
return self.get(path).decode()
def post(self, path: str, body: str) -> bytes:
"""Make a POST request with a string body and return raw bytes."""
try:
conn = self._connect()
conn.request("POST", path, body=body)
res = conn.getresponse()
data = res.read()
if res.status >= 400:
raise BrkError(f"HTTP error: {{res.status}}", res.status)
return data
except (ConnectionError, OSError, TimeoutError) as e:
self._conn = None
raise BrkError(str(e))
def post_json(self, path: str, body: str) -> Any:
"""Make a POST request and return JSON."""
return json.loads(self.post(path, body))
def post_text(self, path: str, body: str) -> str:
"""Make a POST request and return text."""
return self.post(path, body).decode()
def close(self) -> None:
"""Close the HTTP client."""
if self._conn:
@@ -221,7 +243,6 @@ class SeriesData(Generic[T]):
version: int
index: Index
type: str
total: int
start: int
end: int
stamp: str
@@ -326,13 +347,13 @@ AnyDateSeriesData = DateSeriesData[Any]
class _EndpointConfig:
"""Shared endpoint configuration."""
client: BrkClientBase
client: BrkClient
name: str
index: Index
start: Optional[int]
end: Optional[int]
def __init__(self, client: BrkClientBase, name: str, index: Index,
def __init__(self, client: BrkClient, name: str, index: Index,
start: Optional[int] = None, end: Optional[int] = None):
self.client = client
self.name = name
@@ -367,6 +388,12 @@ class _EndpointConfig:
def get_csv(self) -> str:
return self.client.get_text(self._build_path(format='csv'))
def get_len(self) -> int:
return self.client.get_series_len(self.name, self.index)
def get_version(self) -> Version:
return self.client.get_series_version(self.name, self.index)
class RangeBuilder(Generic[T]):
"""Builder with range specified."""
@@ -450,7 +477,7 @@ class SeriesEndpoint(Generic[T]):
data = endpoint.skip(100).take(10).fetch()
"""
def __init__(self, client: BrkClientBase, name: str, index: Index):
def __init__(self, client: BrkClient, name: str, index: Index):
self._config = _EndpointConfig(client, name, index)
@overload
@@ -484,6 +511,14 @@ class SeriesEndpoint(Generic[T]):
"""Fetch all data as CSV."""
return self._config.get_csv()
def len(self) -> int:
"""Total number of data points for this series."""
return self._config.get_len()
def version(self) -> Version:
"""Current version of the series."""
return self._config.get_version()
def path(self) -> str:
"""Get the base endpoint path."""
return self._config.path()
@@ -501,7 +536,7 @@ class DateSeriesEndpoint(Generic[T]):
data = endpoint[:10].fetch()
"""
def __init__(self, client: BrkClientBase, name: str, index: Index):
def __init__(self, client: BrkClient, name: str, index: Index):
self._config = _EndpointConfig(client, name, index)
@overload
@@ -547,6 +582,14 @@ class DateSeriesEndpoint(Generic[T]):
"""Fetch all data as CSV."""
return self._config.get_csv()
def len(self) -> int:
"""Total number of data points for this series."""
return self._config.get_len()
def version(self) -> Version:
"""Current version of the series."""
return self._config.get_version()
def path(self) -> str:
"""Get the base endpoint path."""
return self._config.path()
@@ -605,10 +648,10 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
// Generate helper functions
writeln!(
output,
r#"def _ep(c: BrkClientBase, n: str, i: Index) -> SeriesEndpoint[Any]:
r#"def _ep(c: BrkClient, n: str, i: Index) -> SeriesEndpoint[Any]:
return SeriesEndpoint(c, n, i)
def _dep(c: BrkClientBase, n: str, i: Index) -> DateSeriesEndpoint[Any]:
def _dep(c: BrkClient, n: str, i: Index) -> DateSeriesEndpoint[Any]:
return DateSeriesEndpoint(c, n, i)
"#
)
@@ -624,7 +667,7 @@ def _dep(c: BrkClientBase, n: str, i: Index) -> DateSeriesEndpoint[Any]:
writeln!(output, "class {}(Generic[T]):", by_class_name).unwrap();
writeln!(
output,
" def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n"
" def __init__(self, c: BrkClient, n: str): self._c, self._n = c, n"
)
.unwrap();
for index in &pattern.indexes {
@@ -649,7 +692,7 @@ def _dep(c: BrkClientBase, n: str, i: Index) -> DateSeriesEndpoint[Any]:
writeln!(output, " by: {}[T]", by_class_name).unwrap();
writeln!(
output,
" def __init__(self, c: BrkClientBase, n: str): self._n, self.by = n, {}(c, n)",
" def __init__(self, c: BrkClient, n: str): self._n, self.by = n, {}(c, n)",
by_class_name
)
.unwrap();
@@ -706,13 +749,13 @@ pub fn generate_structural_patterns(
if pattern.is_templated() {
writeln!(
output,
" def __init__(self, client: BrkClientBase, acc: str, disc: str):"
" def __init__(self, client: BrkClient, acc: str, disc: str):"
)
.unwrap();
} else {
writeln!(
output,
" def __init__(self, client: BrkClientBase, acc: str):"
" def __init__(self, client: BrkClient, acc: str):"
)
.unwrap();
}
@@ -64,7 +64,7 @@ fn generate_tree_class(
writeln!(output, " ").unwrap();
writeln!(
output,
" def __init__(self, client: BrkClientBase, base_path: str = ''):"
" def __init__(self, client: BrkClient, base_path: str = ''):"
)
.unwrap();
+190 -114
View File
@@ -87,124 +87,200 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
if !endpoint.should_generate() {
continue;
}
let method_name = endpoint_to_method_name(endpoint);
let base_return_type = endpoint
.response_type
.as_deref()
.map(js_type_to_rust)
.unwrap_or_else(|| "String".to_string());
let return_type = if endpoint.supports_csv {
format!("FormatResponse<{}>", base_return_type)
} else {
base_return_type.clone()
};
writeln!(
output,
" /// {}",
endpoint.summary.as_deref().unwrap_or(&method_name)
)
.unwrap();
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " ///").unwrap();
write_description(output, desc, " /// ", " ///");
match endpoint.method.as_str() {
"GET" => generate_get_method(output, endpoint),
"POST" => generate_post_method(output, endpoint),
_ => continue,
}
// Add endpoint path
writeln!(output, " ///").unwrap();
writeln!(
output,
" /// Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
let params = build_method_params(endpoint);
writeln!(
output,
" pub fn {}(&self{}) -> Result<{}> {{",
method_name, params, return_type
)
.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.{}(&format!(\"{}\"{}))",
fetch_method, path, index_arg
)
.unwrap();
} else {
writeln!(output, " let mut query = Vec::new();").unwrap();
for param in &endpoint.query_params {
let ident = sanitize_ident(&param.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, ident
)
.unwrap();
} else {
writeln!(
output,
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
ident, param.name
)
.unwrap();
}
}
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
writeln!(
output,
" let path = format!(\"{}{{}}\"{}, query_str);",
path, index_arg
)
.unwrap();
if endpoint.supports_csv {
writeln!(output, " if format == Some(Format::CSV) {{").unwrap();
writeln!(
output,
" self.base.get_text(&path).map(FormatResponse::Csv)"
)
.unwrap();
writeln!(output, " }} else {{").unwrap();
writeln!(
output,
" self.base.{}(&path).map(FormatResponse::Json)",
fetch_method
)
.unwrap();
writeln!(output, " }}").unwrap();
} else {
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
}
}
writeln!(output, " }}\n").unwrap();
}
}
fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
let params = build_method_params(endpoint);
writeln!(
output,
" pub fn {}(&self{}) -> Result<{}> {{",
method_name, params, return_type
)
.unwrap();
let (path, index_arg) = build_path_template(endpoint);
let fetch_method = if endpoint.returns_binary() {
"get_bytes"
} else if endpoint.returns_json() {
"get_json"
} else {
"get_text"
};
if endpoint.query_params.is_empty() {
writeln!(
output,
" self.base.{}(&format!(\"{}\"{}))",
fetch_method, path, index_arg
)
.unwrap();
} else {
write_query_assembly(output, endpoint, &path, index_arg);
if endpoint.supports_csv {
writeln!(output, " if format == Some(Format::CSV) {{").unwrap();
writeln!(
output,
" self.base.get_text(&path).map(FormatResponse::Csv)"
)
.unwrap();
writeln!(output, " }} else {{").unwrap();
writeln!(
output,
" self.base.{}(&path).map(FormatResponse::Json)",
fetch_method
)
.unwrap();
writeln!(output, " }}").unwrap();
} else {
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
}
}
writeln!(output, " }}\n").unwrap();
}
fn generate_post_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
let mut params = build_method_params(endpoint);
if endpoint.request_body.is_some() {
params.push_str(", body: &str");
}
writeln!(
output,
" pub fn {}(&self{}) -> Result<{}> {{",
method_name, params, return_type
)
.unwrap();
let (path, index_arg) = build_path_template(endpoint);
let body_arg = if endpoint.request_body.is_some() {
"body"
} else {
"\"\""
};
let fetch_method = if endpoint.returns_binary() {
"post_bytes"
} else if endpoint.returns_json() {
"post_json"
} else {
"post_text"
};
if endpoint.query_params.is_empty() {
writeln!(
output,
" self.base.{}(&format!(\"{}\"{}), {})",
fetch_method, path, index_arg, body_arg
)
.unwrap();
} else {
write_query_assembly(output, endpoint, &path, index_arg);
writeln!(
output,
" self.base.{}(&path, {})",
fetch_method, body_arg
)
.unwrap();
}
writeln!(output, " }}\n").unwrap();
}
fn build_return_type(endpoint: &Endpoint) -> String {
let base = if endpoint.returns_binary() {
"Vec<u8>".to_string()
} else if endpoint.returns_text() {
"String".to_string()
} else {
endpoint
.schema_name()
.map(js_type_to_rust)
.unwrap_or_else(|| "String".to_string())
};
if endpoint.supports_csv {
format!("FormatResponse<{}>", base)
} else {
base
}
}
fn write_method_doc(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
writeln!(
output,
" /// {}",
endpoint.summary.as_deref().unwrap_or(&method_name)
)
.unwrap();
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " ///").unwrap();
write_description(output, desc, " /// ", " ///");
}
writeln!(output, " ///").unwrap();
writeln!(
output,
" /// Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
}
fn write_query_assembly(output: &mut String, endpoint: &Endpoint, path: &str, index_arg: &str) {
writeln!(output, " let mut query = Vec::new();").unwrap();
for param in &endpoint.query_params {
let ident = sanitize_ident(&param.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, ident
)
.unwrap();
} else {
writeln!(
output,
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
ident, param.name
)
.unwrap();
}
}
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
writeln!(
output,
" let path = format!(\"{}{{}}\"{}, query_str);",
path, index_arg
)
.unwrap();
}
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
to_snake_case(&endpoint.operation_name())
}
@@ -103,6 +103,38 @@ impl BrkClientBase {{
.and_then(|mut r| r.body_mut().read_to_string())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a GET request and return raw bytes response.
pub fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {{
self.agent.get(&self.url(path))
.call()
.and_then(|mut r| r.body_mut().read_to_vec())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a POST request and deserialize JSON response.
pub fn post_json<T: DeserializeOwned>(&self, path: &str, body: &str) -> Result<T> {{
self.agent.post(&self.url(path))
.send(body)
.and_then(|mut r| r.body_mut().read_json())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a POST request and return raw text response.
pub fn post_text(&self, path: &str, body: &str) -> Result<String> {{
self.agent.post(&self.url(path))
.send(body)
.and_then(|mut r| r.body_mut().read_to_string())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a POST request and return raw bytes response.
pub fn post_bytes(&self, path: &str, body: &str) -> Result<Vec<u8>> {{
self.agent.post(&self.url(path))
.send(body)
.and_then(|mut r| r.body_mut().read_to_vec())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
}}
/// Build series name with suffix.
@@ -187,6 +219,14 @@ impl EndpointConfig {{
fn get_text(&self, format: Option<&str>) -> Result<String> {{
self.client.get_text(&self.build_path(format))
}}
fn get_len(&self) -> Result<i64> {{
self.client.get_json(&format!("/api/series/{{}}/{{}}/len", self.name, self.index.name()))
}}
fn get_version(&self) -> Result<Version> {{
self.client.get_json(&format!("/api/series/{{}}/{{}}/version", self.name, self.index.name()))
}}
}}
/// Builder for series endpoint queries.
@@ -280,6 +320,17 @@ impl<T: DeserializeOwned, D: DeserializeOwned> SeriesEndpoint<T, D> {{
self.config.get_text(Some("csv"))
}}
/// Total number of data points for this series.
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> Result<i64> {{
self.config.get_len()
}}
/// Current version of the series.
pub fn version(&self) -> Result<Version> {{
self.config.get_version()
}}
/// Get the base endpoint path.
pub fn path(&self) -> String {{
self.config.path()
@@ -25,6 +25,7 @@ pub fn generate_rust_client(
writeln!(output, "// Auto-generated BRK Rust client").unwrap();
writeln!(output, "// Do not edit manually\n").unwrap();
writeln!(output, "#![allow(non_camel_case_types)]").unwrap();
writeln!(output, "#![allow(non_snake_case)]").unwrap();
writeln!(output, "#![allow(dead_code)]").unwrap();
writeln!(output, "#![allow(unused_variables)]").unwrap();
writeln!(output, "#![allow(clippy::useless_format)]").unwrap();
@@ -0,0 +1,92 @@
use crate::openapi::{Parameter, ResponseKind};
/// Request body shape for POST/PUT/PATCH endpoints.
#[derive(Debug, Clone)]
pub struct RequestBody {
/// Body content type as a name (e.g. "string" for text/plain, "Foo" for an `application/json` $ref).
pub body_type: String,
/// Whether the body is required.
pub required: bool,
}
/// Endpoint information extracted from OpenAPI spec.
#[derive(Debug, Clone)]
pub struct Endpoint {
/// HTTP method (GET, POST, etc.)
pub method: String,
/// Path template (e.g., "/blocks/{hash}")
pub path: String,
/// Operation ID (e.g., "getBlockByHash")
pub operation_id: Option<String>,
/// Short summary
pub summary: Option<String>,
/// Detailed description
pub description: Option<String>,
/// Path parameters
pub path_params: Vec<Parameter>,
/// Query parameters
pub query_params: Vec<Parameter>,
/// Request body, if any (POST/PUT/PATCH).
pub request_body: Option<RequestBody>,
/// Body kind for the 200 response.
pub response_kind: ResponseKind,
/// Whether this endpoint is deprecated
pub deprecated: bool,
/// Whether this endpoint supports CSV format (text/csv content type)
pub supports_csv: bool,
}
impl Endpoint {
/// Returns true if this endpoint should be included in client generation.
/// Non-deprecated GET and POST endpoints are included.
pub fn should_generate(&self) -> bool {
!self.deprecated && (self.method == "GET" || self.method == "POST")
}
/// Returns true if this endpoint returns JSON.
pub fn returns_json(&self) -> bool {
matches!(self.response_kind, ResponseKind::Json(_))
}
/// Returns true if this endpoint returns binary data (application/octet-stream).
pub fn returns_binary(&self) -> bool {
matches!(self.response_kind, ResponseKind::Binary)
}
/// Returns true if this endpoint returns plain text (typed or opaque).
pub fn returns_text(&self) -> bool {
matches!(self.response_kind, ResponseKind::Text(_))
}
/// Schema name attached to the response, if any.
pub fn schema_name(&self) -> Option<&str> {
self.response_kind.schema_name()
}
/// 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 {
if let Some(op_id) = &self.operation_id {
return op_id.clone();
}
let mut parts: Vec<String> = Vec::new();
let mut prev_segment = "";
for segment in self.path.split('/').filter(|s| !s.is_empty()) {
if segment == "api" {
continue;
}
if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
let prev_normalized = prev_segment.replace('-', "_");
if !prev_normalized.ends_with(param) {
parts.push(format!("by_{}", param));
}
} else {
let normalized = segment.replace('-', "_");
parts.push(normalized);
prev_segment = segment;
}
}
format!("get_{}", parts.join("_"))
}
}
@@ -1,3 +1,13 @@
mod endpoint;
mod parameter;
mod response_kind;
mod text_schema;
pub use endpoint::{Endpoint, RequestBody};
pub use parameter::Parameter;
pub use response_kind::ResponseKind;
pub use text_schema::TextSchema;
use std::{collections::BTreeMap, io};
use crate::ref_to_type_name;
@@ -11,83 +21,6 @@ use serde_json::Value;
/// Type schema extracted from OpenAPI components
pub type TypeSchemas = BTreeMap<String, Value>;
/// Endpoint information extracted from OpenAPI spec
#[derive(Debug, Clone)]
pub struct Endpoint {
/// HTTP method (GET, POST, etc.)
pub method: String,
/// Path template (e.g., "/blocks/{hash}")
pub path: String,
/// Operation ID (e.g., "getBlockByHash")
pub operation_id: Option<String>,
/// Short summary
pub summary: Option<String>,
/// Detailed description
pub description: Option<String>,
/// Path parameters
pub path_params: Vec<Parameter>,
/// Query parameters
pub query_params: Vec<Parameter>,
/// Response type (simplified)
pub response_type: Option<String>,
/// Whether this endpoint is deprecated
pub deprecated: bool,
/// Whether this endpoint supports CSV format (text/csv content type)
pub supports_csv: bool,
}
impl Endpoint {
/// Returns true if this endpoint should be included in client generation.
/// Only non-deprecated GET endpoints are included.
pub fn should_generate(&self) -> bool {
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 {
if let Some(op_id) = &self.operation_id {
return op_id.clone();
}
// Generate from path: /api/block/{hash} -> "get_block"
// Skip "api" prefix, convert hyphens to underscores, avoid redundant param names
let mut parts: Vec<String> = Vec::new();
let mut prev_segment = "";
for segment in self.path.split('/').filter(|s| !s.is_empty()) {
if segment == "api" {
continue;
}
if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
// Only add "by_{param}" if the previous segment doesn't already contain the param name
let prev_normalized = prev_segment.replace('-', "_");
if !prev_normalized.ends_with(param) {
parts.push(format!("by_{}", param));
}
} else {
let normalized = segment.replace('-', "_");
parts.push(normalized);
prev_segment = segment;
}
}
format!("get_{}", parts.join("_"))
}
}
/// Parameter information
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub required: bool,
pub param_type: String,
pub description: Option<String>,
}
/// Parse OpenAPI spec from JSON string
///
/// Pre-processes the JSON to handle oas3 limitations:
@@ -164,7 +97,7 @@ pub fn extract_endpoints(spec: &Spec) -> Vec<Endpoint> {
for (path, path_item) in paths {
for (method, operation) in get_operations(path_item) {
if let Some(endpoint) = extract_endpoint(path, method, operation) {
if let Some(endpoint) = extract_endpoint(path, method, operation, spec) {
endpoints.push(endpoint);
}
}
@@ -186,11 +119,17 @@ fn get_operations(path_item: &PathItem) -> Vec<(&'static str, &Operation)> {
.collect()
}
fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option<Endpoint> {
fn extract_endpoint(
path: &str,
method: &str,
operation: &Operation,
spec: &Spec,
) -> Option<Endpoint> {
let path_params = extract_path_parameters(path, operation);
let query_params = extract_parameters(operation, ParameterIn::Query);
let response_type = extract_response_type(operation);
let response_kind = extract_response_kind(operation, spec);
let request_body = extract_request_body(operation);
let supports_csv = check_csv_support(operation);
Some(Endpoint {
@@ -201,12 +140,38 @@ fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option<E
description: operation.description.clone(),
path_params,
query_params,
response_type,
request_body,
response_kind,
deprecated: operation.deprecated.unwrap_or(false),
supports_csv,
})
}
/// Extract the request body shape, if any.
/// Prefers `text/plain` (string) over `application/json` (typed).
fn extract_request_body(operation: &Operation) -> Option<RequestBody> {
let req = operation.request_body.as_ref()?;
let req = match req {
ObjectOrReference::Object(rb) => rb,
ObjectOrReference::Ref { .. } => return None,
};
let body_type = if req.content.contains_key("text/plain; charset=utf-8")
|| req.content.contains_key("text/plain")
{
"string".to_string()
} else if let Some(content) = req.content.get("application/json") {
schema_name_from_content(content).unwrap_or_else(|| "Object".to_string())
} else {
"string".to_string()
};
Some(RequestBody {
body_type,
required: req.required.unwrap_or(false),
})
}
/// Check if the endpoint supports CSV format (has text/csv in 200 response content types).
fn check_csv_support(operation: &Operation) -> bool {
let Some(responses) = operation.responses.as_ref() else {
@@ -253,12 +218,7 @@ fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec<Param
let param_type = param
.schema
.as_ref()
.and_then(|s| match s {
ObjectOrReference::Ref { ref_path, .. } => {
ref_to_type_name(ref_path).map(|s| s.to_string())
}
ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
})
.and_then(schema_type_from_schema)
.unwrap_or_else(|| "string".to_string());
Some(Parameter {
name: param.name.clone(),
@@ -272,28 +232,59 @@ fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec<Param
.collect()
}
fn extract_response_type(operation: &Operation) -> Option<String> {
let responses = operation.responses.as_ref()?;
fn extract_response_kind(operation: &Operation, spec: &Spec) -> ResponseKind {
let response = operation
.responses
.as_ref()
.and_then(|r| r.get("200"))
.and_then(|r| match r {
ObjectOrReference::Object(o) => Some(o),
ObjectOrReference::Ref { .. } => None,
});
let Some(response) = response else {
return ResponseKind::Text(None);
};
// Look for 200 OK response
let response = responses.get("200")?;
match response {
ObjectOrReference::Object(response) => {
// Look for JSON content
let content = response.content.get("application/json")?;
match &content.schema {
Some(ObjectOrReference::Ref { ref_path, .. }) => {
// Extract type name from reference like "#/components/schemas/Block"
Some(ref_to_type_name(ref_path)?.to_string())
}
Some(ObjectOrReference::Object(schema)) => schema_to_type_name(schema),
None => None,
}
}
ObjectOrReference::Ref { .. } => None,
if response.content.contains_key("application/octet-stream") {
return ResponseKind::Binary;
}
if let Some(content) = response.content.get("application/json") {
return ResponseKind::Json(
schema_name_from_content(content).unwrap_or_else(|| "*".to_string()),
);
}
if let Some(content) = response.content.get("text/plain; charset=utf-8") {
let schema = schema_name_from_content(content).map(|name| {
let is_numeric = is_numeric_schema(spec, &name);
TextSchema { name, is_numeric }
});
return ResponseKind::Text(schema);
}
ResponseKind::Text(None)
}
fn schema_name_from_content(content: &oas3::spec::MediaType) -> Option<String> {
schema_type_from_schema(content.schema.as_ref()?)
}
/// Resolves `name` against `components.schemas` and reports whether the
/// underlying primitive is `integer` or `number`.
fn is_numeric_schema(spec: &Spec, name: &str) -> bool {
let Some(components) = spec.components.as_ref() else {
return false;
};
let Some(Schema::Object(obj_or_ref)) = components.schemas.get(name) else {
return false;
};
let ObjectOrReference::Object(schema) = obj_or_ref.as_ref() else {
return false;
};
matches!(
schema.schema_type.as_ref(),
Some(SchemaTypeSet::Single(
SchemaType::Integer | SchemaType::Number
))
)
}
fn schema_type_from_schema(schema: &Schema) -> Option<String> {
@@ -337,19 +328,21 @@ fn schema_to_type_name(schema: &ObjectSchema) -> Option<String> {
let types: Vec<String> = variants
.iter()
.filter_map(|v| match v {
ObjectOrReference::Ref { ref_path, .. } => {
ref_to_type_name(ref_path).map(|s| s.to_string())
}
ObjectOrReference::Object(obj) => {
// Skip null variants
if matches!(
obj.schema_type.as_ref(),
Some(SchemaTypeSet::Single(SchemaType::Null))
) {
return None;
Schema::Boolean(_) => None,
Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
ObjectOrReference::Ref { ref_path, .. } => {
ref_to_type_name(ref_path).map(|s| s.to_string())
}
schema_to_type_name(obj)
}
ObjectOrReference::Object(obj) => {
if matches!(
obj.schema_type.as_ref(),
Some(SchemaTypeSet::Single(SchemaType::Null))
) {
return None;
}
schema_to_type_name(obj)
}
},
})
.collect();
@@ -364,7 +357,7 @@ fn single_type_to_name(t: &SchemaType, schema: &ObjectSchema) -> Option<String>
match t {
SchemaType::String => Some("string".to_string()),
SchemaType::Number => Some("number".to_string()),
SchemaType::Integer => Some("number".to_string()),
SchemaType::Integer => Some("integer".to_string()),
SchemaType::Boolean => Some("boolean".to_string()),
SchemaType::Array => {
let inner = match &schema.items {
@@ -0,0 +1,8 @@
/// Parameter information.
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub required: bool,
pub param_type: String,
pub description: Option<String>,
}
@@ -0,0 +1,29 @@
use crate::openapi::TextSchema;
/// 200-response body shape.
#[derive(Debug, Clone)]
pub enum ResponseKind {
/// JSON body, schema named (e.g. "Block").
Json(String),
/// `text/plain` body. `Some(schema)` carries a typed shape (e.g. "Height", "Hex");
/// `None` is the escape hatch for opaque text.
Text(Option<TextSchema>),
/// `application/octet-stream`.
Binary,
}
impl ResponseKind {
/// Schema name, if the body is named (Json or typed Text).
pub fn schema_name(&self) -> Option<&str> {
match self {
Self::Json(s) => Some(s.as_str()),
Self::Text(Some(t)) => Some(t.name.as_str()),
_ => None,
}
}
/// True when a typed text body needs numeric parsing (`int(...)` etc.).
pub fn text_is_numeric(&self) -> bool {
matches!(self, Self::Text(Some(t)) if t.is_numeric)
}
}
@@ -0,0 +1,8 @@
/// Schema metadata for a typed `text/plain` response.
#[derive(Debug, Clone)]
pub struct TextSchema {
/// Schema name, e.g. "Height", "Hex".
pub name: String,
/// True when the underlying primitive is `integer`/`number` (body needs numeric parsing).
pub is_numeric: bool,
}
-3
View File
@@ -31,6 +31,3 @@ vecdb = { workspace = true }
[[bin]]
name = "brk"
path = "src/main.rs"
[package.metadata.dist]
dist = true
+7 -2
View File
@@ -19,16 +19,21 @@ BRK uses [sparse files](https://en.wikipedia.org/wiki/Sparse_file). Tools like `
## Install
```bash
rustup update
RUSTFLAGS="-C target-cpu=native" cargo install --locked brk_cli
rustup update && RUSTFLAGS="-C target-cpu=native" cargo install --locked brk_cli --version $(cargo search brk_cli | head -1 | awk -F'"' '{print $2}')
```
Updates Rust, then builds `brk_cli` with optimizations tuned to your CPU. The `--version $(...)` subshell queries crates.io for the absolute latest published version, including pre-releases (rc/beta/alpha); without it, `cargo install` only picks the latest stable.
Portable build (without native CPU optimizations):
```bash
cargo install --locked brk_cli
```
## Update
Updating is the same as installing: re-run the install command above. `cargo install` overwrites the existing binary in place. Indexed data is reused when the on-disk format is unchanged, otherwise it is reset and re-synced automatically on the next run.
## Run
```bash
+12 -32
View File
@@ -5,9 +5,7 @@ use std::{
use brk_error::{Error, Result};
use brk_rpc::{Auth, Client};
use brk_server::{
CdnCacheMode, DEFAULT_CACHE_SIZE, DEFAULT_MAX_WEIGHT, DEFAULT_MAX_WEIGHT_LOCALHOST, Website,
};
use brk_server::{CdnCacheMode, DEFAULT_MAX_UTXOS, DEFAULT_MAX_WEIGHT, Website};
use brk_types::Port;
use owo_colors::OwoColorize;
use serde::{Deserialize, Serialize};
@@ -33,10 +31,7 @@ pub struct Config {
maxweight: Option<usize>,
#[serde(default)]
maxweightlocal: Option<usize>,
#[serde(default)]
cachesize: Option<usize>,
maxutxos: Option<usize>,
#[serde(default)]
bitcoindir: Option<String>,
@@ -87,11 +82,8 @@ impl Config {
if let Some(v) = config_args.maxweight {
config.maxweight = Some(v);
}
if let Some(v) = config_args.maxweightlocal {
config.maxweightlocal = Some(v);
}
if let Some(v) = config_args.cachesize {
config.cachesize = Some(v);
if let Some(v) = config_args.maxutxos {
config.maxutxos = Some(v);
}
if let Some(v) = config_args.bitcoindir {
config.bitcoindir = Some(v);
@@ -143,11 +135,8 @@ impl Config {
Long("maxweight") => {
config.maxweight = Some(parser.value().unwrap().parse().unwrap())
}
Long("maxweightlocal") => {
config.maxweightlocal = Some(parser.value().unwrap().parse().unwrap())
}
Long("cachesize") => {
config.cachesize = Some(parser.value().unwrap().parse().unwrap())
Long("maxutxos") => {
config.maxutxos = Some(parser.value().unwrap().parse().unwrap())
}
Long("bitcoindir") => {
config.bitcoindir = Some(parser.value().unwrap().parse().unwrap())
@@ -214,19 +203,14 @@ impl Config {
"[false]".bright_black()
);
println!(
" --maxweight {} Max series response weight in bytes for external clients {}",
" --maxweight {} Server cap on series response weight in bytes; rejects /api/{{series,metric}}/... over the limit {}",
"<BYTES>".bright_black(),
format!("[{}]", DEFAULT_MAX_WEIGHT).bright_black()
);
println!(
" --maxweightlocal {} Max series response weight in bytes for loopback clients {}",
"<BYTES>".bright_black(),
format!("[{}]", DEFAULT_MAX_WEIGHT_LOCALHOST).bright_black()
);
println!(
" --cachesize {} LRU capacity for the in-process response cache {}",
"<ENTRIES>".bright_black(),
format!("[{}]", DEFAULT_CACHE_SIZE).bright_black()
" --maxutxos {} Server cap on UTXOs per address; /api/address/{{addr}}/utxo errors past the limit {}",
"<COUNT>".bright_black(),
format!("[{}]", DEFAULT_MAX_UTXOS).bright_black()
);
println!();
println!(
@@ -410,12 +394,8 @@ Finally, you can run the program with '-h' for help."
self.maxweight.unwrap_or(DEFAULT_MAX_WEIGHT)
}
pub fn max_weight_localhost(&self) -> usize {
self.maxweightlocal.unwrap_or(DEFAULT_MAX_WEIGHT_LOCALHOST)
}
pub fn cache_size(&self) -> usize {
self.cachesize.unwrap_or(DEFAULT_CACHE_SIZE)
pub fn max_utxos(&self) -> usize {
self.maxutxos.unwrap_or(DEFAULT_MAX_UTXOS)
}
pub fn brkport(&self) -> Option<Port> {
+11 -12
View File
@@ -42,7 +42,7 @@ pub fn main() -> anyhow::Result<()> {
{
// 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();
let indexed_height = indexer.vecs.next_height();
let blocks_behind = chain_height.saturating_sub(*indexed_height);
if blocks_behind > 10_000 {
info!("---");
@@ -63,11 +63,9 @@ pub fn main() -> anyhow::Result<()> {
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool.clone()));
let mempool_clone = mempool.clone();
let query_clone = query.clone();
let resolver = query.sync(|q| q.indexer_prevout_resolver());
thread::spawn(move || {
mempool_clone.start_with(|| {
query_clone.sync(|q| q.fill_mempool_prevouts());
});
mempool_clone.start_with(resolver);
});
let server_config = ServerConfig {
@@ -75,8 +73,7 @@ pub fn main() -> anyhow::Result<()> {
website: config.website(),
cdn_cache_mode: config.cdn_cache_mode(),
max_weight: config.max_weight(),
max_weight_localhost: config.max_weight_localhost(),
cache_size: config.cache_size(),
max_utxos: config.max_utxos(),
};
let port = config.brkport();
@@ -106,15 +103,17 @@ pub fn main() -> anyhow::Result<()> {
let total_start = Instant::now();
let starting_indexes = if cfg!(debug_assertions) {
indexer.checked_index(&reader, &client, &exit)?
if cfg!(debug_assertions) {
indexer.checked_index(&reader, &client, &exit)?;
} else {
indexer.index(&reader, &client, &exit)?
};
indexer.index(&reader, &client, &exit)?;
}
Mimalloc::collect();
computer.compute(&indexer, starting_indexes, &exit)?;
computer.compute(&indexer, &exit)?;
indexer.advance_safe_lengths()?;
info!("Total time: {:?}", total_start.elapsed());
info!("Waiting for new blocks...");
+1 -1
View File
@@ -6,7 +6,7 @@ pub fn dot_brk_path() -> PathBuf {
}
pub fn dot_brk_log_path() -> PathBuf {
dot_brk_path().join("log")
dot_brk_path().join("logs")
}
pub fn default_brk_path() -> PathBuf {
+1 -1
View File
@@ -17,7 +17,7 @@ fn main() -> brk_client::Result<()> {
// day1() returns DateMetricEndpointBuilder, so fetch() returns DateMetricData
let price_close = client
.series()
.prices
.price
.split
.close
.usd
+2540 -5868
View File
File diff suppressed because it is too large Load Diff
+104
View File
@@ -0,0 +1,104 @@
use brk_traversable::Traversable;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Serialize;
use super::{CohortName, Filter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryPrice {
Discount,
Premium,
}
impl EntryPrice {
#[inline]
pub const fn from_is_discount(is_discount: bool) -> Self {
if is_discount {
Self::Discount
} else {
Self::Premium
}
}
#[inline]
pub const fn is_discount(self) -> bool {
matches!(self, Self::Discount)
}
}
pub const ENTRY_FILTERS: ByEntry<Filter> = ByEntry {
discount: Filter::Entry(EntryPrice::Discount),
premium: Filter::Entry(EntryPrice::Premium),
};
pub const ENTRY_NAMES: ByEntry<CohortName> = ByEntry {
discount: CohortName::new("veteran", "Veteran", "Veteran Coins"),
premium: CohortName::new("rookie", "Rookie", "Rookie Coins"),
};
#[derive(Default, Clone, Traversable, Serialize)]
pub struct ByEntry<T> {
pub discount: T,
pub premium: T,
}
impl ByEntry<CohortName> {
pub const fn names() -> &'static Self {
&ENTRY_NAMES
}
}
impl<T> ByEntry<T> {
pub fn new<F>(mut create: F) -> Self
where
F: FnMut(Filter, &'static str) -> T,
{
let f = ENTRY_FILTERS;
let n = ENTRY_NAMES;
Self {
discount: create(f.discount, n.discount.id),
premium: create(f.premium, n.premium.id),
}
}
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
where
F: FnMut(Filter, &'static str) -> Result<T, E>,
{
let f = ENTRY_FILTERS;
let n = ENTRY_NAMES;
Ok(Self {
discount: create(f.discount, n.discount.id)?,
premium: create(f.premium, n.premium.id)?,
})
}
pub fn get(&self, entry: EntryPrice) -> &T {
match entry {
EntryPrice::Discount => &self.discount,
EntryPrice::Premium => &self.premium,
}
}
pub fn get_mut(&mut self, entry: EntryPrice) -> &mut T {
match entry {
EntryPrice::Discount => &mut self.discount,
EntryPrice::Premium => &mut self.premium,
}
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
[&self.discount, &self.premium].into_iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[&mut self.discount, &mut self.premium].into_iter()
}
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
where
T: Send + Sync,
{
[&mut self.discount, &mut self.premium].into_par_iter()
}
}
+2 -1
View File
@@ -24,7 +24,7 @@ impl CohortContext {
/// Build full name for a filter, adding prefix only for Time/Amount filters.
///
/// Prefix rules:
/// - No prefix: `All`, `Term`, `Epoch`, `Class`, `Type`
/// - No prefix: `All`, `Term`, `Epoch`, `Class`, `Entry`, `Type`
/// - Context prefix: `Time`, `Amount`
pub fn full_name(&self, filter: &Filter, name: &str) -> String {
match filter {
@@ -32,6 +32,7 @@ impl CohortContext {
| Filter::Term(_)
| Filter::Epoch(_)
| Filter::Class(_)
| Filter::Entry(_)
| Filter::Type(_) => name.to_string(),
Filter::Time(_) | Filter::Amount(_) => self.prefixed(name),
}
+8 -3
View File
@@ -1,6 +1,6 @@
use brk_types::{Halving, OutputType, Sats, Year};
use super::{AmountFilter, CohortContext, Term, TimeFilter};
use super::{AmountFilter, CohortContext, EntryPrice, Term, TimeFilter};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Filter {
@@ -10,6 +10,7 @@ pub enum Filter {
Amount(AmountFilter),
Epoch(Halving),
Class(Year),
Entry(EntryPrice),
Type(OutputType),
}
@@ -68,7 +69,8 @@ impl Filter {
}
/// Whether to compute extended metrics (realized cap ratios, profit/loss ratios, percentiles)
/// For UTXO context: true only for age range cohorts (Range) and aggregate cohorts (All, Term)
/// For UTXO context: true for age range cohorts (Range), aggregate cohorts (All, Term),
/// and immutable entry valuation cohorts.
/// For address context: always false
pub fn is_extended(&self, context: CohortContext) -> bool {
match context {
@@ -76,7 +78,10 @@ impl Filter {
CohortContext::Utxo => {
matches!(
self,
Filter::All | Filter::Term(_) | Filter::Time(TimeFilter::Range(_))
Filter::All
| Filter::Term(_)
| Filter::Time(TimeFilter::Range(_))
| Filter::Entry(_)
)
}
}
+2
View File
@@ -7,6 +7,7 @@ mod amount_range;
mod by_addr_type;
mod by_any_addr;
mod by_epoch;
mod by_entry;
mod by_term;
mod by_type;
mod class;
@@ -36,6 +37,7 @@ pub use amount_range::*;
pub use by_addr_type::*;
pub use by_any_addr::*;
pub use by_epoch::*;
pub use by_entry::*;
pub use by_term::*;
pub use by_type::*;
pub use class::*;
+10 -2
View File
@@ -2,8 +2,8 @@ use brk_traversable::Traversable;
use rayon::prelude::*;
use crate::{
AgeRange, AmountRange, ByEpoch, ByTerm, Class, Filter, OverAge, OverAmount, SpendableType,
UnderAge, UnderAmount,
AgeRange, AmountRange, ByEntry, ByEpoch, ByTerm, Class, Filter, OverAge, OverAmount,
SpendableType, UnderAge, UnderAmount,
};
#[derive(Default, Clone, Traversable)]
@@ -12,6 +12,7 @@ pub struct UTXOGroups<T> {
pub age_range: AgeRange<T>,
pub epoch: ByEpoch<T>,
pub class: Class<T>,
pub entry: ByEntry<T>,
pub over_age: OverAge<T>,
pub over_amount: OverAmount<T>,
pub amount_range: AmountRange<T>,
@@ -31,6 +32,7 @@ impl<T> UTXOGroups<T> {
age_range: AgeRange::new(&mut create),
epoch: ByEpoch::new(&mut create),
class: Class::new(&mut create),
entry: ByEntry::new(&mut create),
over_age: OverAge::new(&mut create),
over_amount: OverAmount::new(&mut create),
amount_range: AmountRange::new(&mut create),
@@ -51,6 +53,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.iter())
.chain(self.epoch.iter())
.chain(self.class.iter())
.chain(self.entry.iter())
.chain(self.amount_range.iter())
.chain(self.under_amount.iter())
.chain(self.type_.iter())
@@ -66,6 +69,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.iter_mut())
.chain(self.epoch.iter_mut())
.chain(self.class.iter_mut())
.chain(self.entry.iter_mut())
.chain(self.amount_range.iter_mut())
.chain(self.under_amount.iter_mut())
.chain(self.type_.iter_mut())
@@ -84,6 +88,7 @@ impl<T> UTXOGroups<T> {
.chain(self.age_range.par_iter_mut())
.chain(self.epoch.par_iter_mut())
.chain(self.class.par_iter_mut())
.chain(self.entry.par_iter_mut())
.chain(self.amount_range.par_iter_mut())
.chain(self.under_amount.par_iter_mut())
.chain(self.type_.par_iter_mut())
@@ -94,6 +99,7 @@ impl<T> UTXOGroups<T> {
.iter()
.chain(self.epoch.iter())
.chain(self.class.iter())
.chain(self.entry.iter())
.chain(self.amount_range.iter())
.chain(self.type_.iter())
}
@@ -103,6 +109,7 @@ impl<T> UTXOGroups<T> {
.iter_mut()
.chain(self.epoch.iter_mut())
.chain(self.class.iter_mut())
.chain(self.entry.iter_mut())
.chain(self.amount_range.iter_mut())
.chain(self.type_.iter_mut())
}
@@ -115,6 +122,7 @@ impl<T> UTXOGroups<T> {
.par_iter_mut()
.chain(self.epoch.par_iter_mut())
.chain(self.class.par_iter_mut())
.chain(self.entry.par_iter_mut())
.chain(self.amount_range.par_iter_mut())
.chain(self.type_.par_iter_mut())
}
+1 -1
View File
@@ -22,7 +22,7 @@ Compute 1000+ on-chain metrics from indexed blockchain data: supply breakdowns,
let mut computer = Computer::forced_import(&outputs_path, &indexer)?;
// Compute all metrics for new blocks
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
computer.compute(&indexer, &exit)?;
// Access computed data via traversable vecs
let supply = computer.distribution.utxo_cohorts.all.metrics.supply.total.sats.height;
+3 -3
View File
@@ -37,7 +37,7 @@ pub fn main() -> color_eyre::Result<()> {
// 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();
let indexed_height = indexer.vecs.next_height();
if u32::from(chain_height).saturating_sub(u32::from(indexed_height)) > 1000 {
indexer.checked_index(&reader, &client, &exit)?;
drop(indexer);
@@ -49,11 +49,11 @@ pub fn main() -> color_eyre::Result<()> {
loop {
let i = Instant::now();
let starting_indexes = indexer.checked_index(&reader, &client, &exit)?;
indexer.checked_index(&reader, &client, &exit)?;
Mimalloc::collect();
computer.compute(&indexer, starting_indexes, &exit)?;
computer.compute(&indexer, &exit)?;
dbg!(i.elapsed());
sleep(Duration::from_secs(10));
}
@@ -44,13 +44,13 @@ pub fn main() -> Result<()> {
});
let i = Instant::now();
let starting_indexes = indexer.index(&reader, &client, &exit)?;
indexer.index(&reader, &client, &exit)?;
info!("Done in {:?}", i.elapsed());
Mimalloc::collect();
let i = Instant::now();
computer.compute(&indexer, starting_indexes, &exit)?;
computer.compute(&indexer, &exit)?;
info!("Done in {:?}", i.elapsed());
// We want to benchmark the drop too
+3 -3
View File
@@ -48,7 +48,7 @@ pub fn main() -> color_eyre::Result<()> {
// 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();
let indexed_height = indexer.vecs.next_height();
if chain_height.saturating_sub(*indexed_height) > 1000 {
indexer.index(&reader, &client, &exit)?;
drop(indexer);
@@ -60,13 +60,13 @@ pub fn main() -> color_eyre::Result<()> {
loop {
let i = Instant::now();
let starting_indexes = indexer.index(&reader, &client, &exit)?;
indexer.index(&reader, &client, &exit)?;
info!("Done in {:?}", i.elapsed());
Mimalloc::collect();
let i = Instant::now();
computer.compute(&indexer, starting_indexes, &exit)?;
computer.compute(&indexer, &exit)?;
info!("Done in {:?}", i.elapsed());
sleep(Duration::from_secs(60));
+7 -9
View File
@@ -2,7 +2,6 @@ use std::thread;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::Indexes;
use vecdb::Exit;
use crate::indexes;
@@ -14,13 +13,12 @@ impl Vecs {
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
self.db.sync_bg_tasks()?;
// lookback depends on indexes.timestamp.monotonic
self.lookback.compute(indexes, starting_indexes, exit)?;
self.lookback.compute(indexer, indexes, exit)?;
// Parallel: remaining sub-modules are independent of each other.
// size depends on lookback (already computed above).
@@ -35,12 +33,12 @@ impl Vecs {
..
} = self;
thread::scope(|s| -> Result<()> {
let r1 = s.spawn(|| count.compute(indexer, starting_indexes, exit));
let r2 = s.spawn(|| interval.compute(indexer, starting_indexes, exit));
let r3 = s.spawn(|| weight.compute(indexer, starting_indexes, exit));
let r4 = s.spawn(|| difficulty.compute(indexer, indexes, starting_indexes, exit));
let r5 = s.spawn(|| halving.compute(indexes, starting_indexes, exit));
size.compute(indexer, &*lookback, starting_indexes, exit)?;
let r1 = s.spawn(|| count.compute(indexer, exit));
let r2 = s.spawn(|| interval.compute(indexer, exit));
let r3 = s.spawn(|| weight.compute(indexer, exit));
let r4 = s.spawn(|| difficulty.compute(indexer, indexes, exit));
let r5 = s.spawn(|| halving.compute(indexer, indexes, exit));
size.compute(indexer, &*lookback, exit)?;
r1.join().unwrap()?;
r2.join().unwrap()?;
r3.join().unwrap()?;
@@ -1,25 +1,20 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Indexes, StoredU32};
use brk_types::StoredU32;
use vecdb::Exit;
use super::Vecs;
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
// Block count raw + cumulative
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
self.total.block.compute_range(
starting_indexes.height,
starting_height,
&indexer.vecs.blocks.weight,
|h| (h, StoredU32::from(1_u32)),
exit,
)?;
self.total.compute_rest(starting_indexes.height, exit)?;
self.total.compute_rest(starting_height, exit)?;
Ok(())
}
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Indexes, StoredU32};
use brk_types::StoredU32;
use vecdb::Exit;
use super::Vecs;
@@ -11,25 +11,25 @@ impl Vecs {
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
self.adjustment.bps.height.compute_ratio_change(
starting_indexes.height,
starting_height,
&indexer.vecs.blocks.difficulty,
2016,
exit,
)?;
self.epoch.height.compute_transform(
starting_indexes.height,
starting_height,
&indexes.height.epoch,
|(h, epoch, ..)| (h, epoch),
exit,
)?;
self.blocks_to_retarget.height.compute_transform(
starting_indexes.height,
starting_height,
&indexes.height.epoch,
|(h, ..)| (h, StoredU32::from(h.left_before_next_diff_adj())),
exit,
@@ -1,5 +1,6 @@
use brk_error::Result;
use brk_types::{Indexes, StoredU32};
use brk_indexer::Indexer;
use brk_types::StoredU32;
use vecdb::Exit;
use super::Vecs;
@@ -8,19 +9,20 @@ use crate::indexes;
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
self.epoch.height.compute_transform(
starting_indexes.height,
starting_height,
&indexes.height.halving,
|(h, epoch, ..)| (h, epoch),
exit,
)?;
self.blocks_to_halving.height.compute_transform(
starting_indexes.height,
starting_height,
&indexes.height.halving,
|(h, ..)| (h, StoredU32::from(h.left_before_next_halving())),
exit,
@@ -1,21 +1,17 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{CheckedSub, Indexes, Timestamp};
use brk_types::{CheckedSub, Timestamp};
use vecdb::{Exit, ReadableVec};
use super::Vecs;
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let mut prev_timestamp = None;
self.0.compute(starting_indexes.height, exit, |vec| {
self.0.compute(starting_height, exit, |vec| {
vec.compute_transform(
starting_indexes.height,
starting_height,
&indexer.vecs.blocks.timestamp,
|(h, timestamp, ..)| {
let interval = if let Some(prev_h) = h.decremented() {
+58 -64
View File
@@ -1,6 +1,7 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Timestamp, Version};
use brk_types::{Height, Timestamp, Version};
use vecdb::{
AnyVec, CachedVec, Cursor, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableVec, Rw,
StorageMode, VecIndex,
@@ -219,53 +220,54 @@ impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
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.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.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)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 13, |s| &mut s._13d)?;
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.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)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 63, |s| &mut s._9w)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 84, |s| &mut s._12w)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 89, |s| &mut s._89d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 90, |s| &mut s._3m)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 98, |s| &mut s._14w)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 111, |s| &mut s._111d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 144, |s| &mut s._144d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 180, |s| &mut s._6m)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 182, |s| &mut s._26w)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 200, |s| &mut s._200d)?;
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.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)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 1095, |s| &mut s._3y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 1400, |s| &mut s._200w)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 1460, |s| &mut s._4y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 1825, |s| &mut s._5y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 2190, |s| &mut s._6y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 2920, |s| &mut s._8y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 3285, |s| &mut s._9y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 3650, |s| &mut s._10y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 4380, |s| &mut s._12y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 5110, |s| &mut s._14y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 9490, |s| &mut s._26y)?;
let starting_height = indexer.safe_lengths().height;
self.compute_rolling_start_hours(indexes, starting_height, exit, 1, |s| &mut s._1h)?;
self.compute_rolling_start(indexes, starting_height, exit, 1, |s| &mut s._24h.inner)?;
self.compute_rolling_start(indexes, starting_height, exit, 3, |s| &mut s._3d)?;
self.compute_rolling_start(indexes, starting_height, exit, 7, |s| &mut s._1w.inner)?;
self.compute_rolling_start(indexes, starting_height, exit, 8, |s| &mut s._8d)?;
self.compute_rolling_start(indexes, starting_height, exit, 9, |s| &mut s._9d)?;
self.compute_rolling_start(indexes, starting_height, exit, 12, |s| &mut s._12d)?;
self.compute_rolling_start(indexes, starting_height, exit, 13, |s| &mut s._13d)?;
self.compute_rolling_start(indexes, starting_height, exit, 14, |s| &mut s._2w)?;
self.compute_rolling_start(indexes, starting_height, exit, 21, |s| &mut s._21d)?;
self.compute_rolling_start(indexes, starting_height, exit, 26, |s| &mut s._26d)?;
self.compute_rolling_start(indexes, starting_height, exit, 30, |s| &mut s._1m.inner)?;
self.compute_rolling_start(indexes, starting_height, exit, 34, |s| &mut s._34d)?;
self.compute_rolling_start(indexes, starting_height, exit, 55, |s| &mut s._55d)?;
self.compute_rolling_start(indexes, starting_height, exit, 60, |s| &mut s._2m)?;
self.compute_rolling_start(indexes, starting_height, exit, 63, |s| &mut s._9w)?;
self.compute_rolling_start(indexes, starting_height, exit, 84, |s| &mut s._12w)?;
self.compute_rolling_start(indexes, starting_height, exit, 89, |s| &mut s._89d)?;
self.compute_rolling_start(indexes, starting_height, exit, 90, |s| &mut s._3m)?;
self.compute_rolling_start(indexes, starting_height, exit, 98, |s| &mut s._14w)?;
self.compute_rolling_start(indexes, starting_height, exit, 111, |s| &mut s._111d)?;
self.compute_rolling_start(indexes, starting_height, exit, 144, |s| &mut s._144d)?;
self.compute_rolling_start(indexes, starting_height, exit, 180, |s| &mut s._6m)?;
self.compute_rolling_start(indexes, starting_height, exit, 182, |s| &mut s._26w)?;
self.compute_rolling_start(indexes, starting_height, exit, 200, |s| &mut s._200d)?;
self.compute_rolling_start(indexes, starting_height, exit, 270, |s| &mut s._9m)?;
self.compute_rolling_start(indexes, starting_height, exit, 350, |s| &mut s._350d)?;
self.compute_rolling_start(indexes, starting_height, exit, 360, |s| &mut s._12m)?;
self.compute_rolling_start(indexes, starting_height, exit, 365, |s| &mut s._1y.inner)?;
self.compute_rolling_start(indexes, starting_height, exit, 420, |s| &mut s._14m)?;
self.compute_rolling_start(indexes, starting_height, exit, 730, |s| &mut s._2y)?;
self.compute_rolling_start(indexes, starting_height, exit, 780, |s| &mut s._26m)?;
self.compute_rolling_start(indexes, starting_height, exit, 1095, |s| &mut s._3y)?;
self.compute_rolling_start(indexes, starting_height, exit, 1400, |s| &mut s._200w)?;
self.compute_rolling_start(indexes, starting_height, exit, 1460, |s| &mut s._4y)?;
self.compute_rolling_start(indexes, starting_height, exit, 1825, |s| &mut s._5y)?;
self.compute_rolling_start(indexes, starting_height, exit, 2190, |s| &mut s._6y)?;
self.compute_rolling_start(indexes, starting_height, exit, 2920, |s| &mut s._8y)?;
self.compute_rolling_start(indexes, starting_height, exit, 3285, |s| &mut s._9y)?;
self.compute_rolling_start(indexes, starting_height, exit, 3650, |s| &mut s._10y)?;
self.compute_rolling_start(indexes, starting_height, exit, 4380, |s| &mut s._12y)?;
self.compute_rolling_start(indexes, starting_height, exit, 5110, |s| &mut s._14y)?;
self.compute_rolling_start(indexes, starting_height, exit, 9490, |s| &mut s._26y)?;
Ok(())
}
@@ -273,7 +275,7 @@ impl Vecs {
fn compute_rolling_start<F>(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
starting_height: Height,
exit: &Exit,
days: usize,
get_field: F,
@@ -281,19 +283,15 @@ impl Vecs {
where
F: FnOnce(&mut Self) -> &mut EagerVec<PcoVec<Height, Height>>,
{
self.compute_rolling_start_inner(
indexes,
starting_indexes,
exit,
get_field,
|t, prev_ts| t.difference_in_days_between(prev_ts) >= days,
)
self.compute_rolling_start_inner(indexes, starting_height, exit, get_field, |t, prev_ts| {
t.difference_in_days_between(prev_ts) >= days
})
}
fn compute_rolling_start_hours<F>(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
starting_height: Height,
exit: &Exit,
hours: usize,
get_field: F,
@@ -301,19 +299,15 @@ impl Vecs {
where
F: FnOnce(&mut Self) -> &mut EagerVec<PcoVec<Height, Height>>,
{
self.compute_rolling_start_inner(
indexes,
starting_indexes,
exit,
get_field,
|t, prev_ts| t.difference_in_hours_between(prev_ts) >= hours,
)
self.compute_rolling_start_inner(indexes, starting_height, exit, get_field, |t, prev_ts| {
t.difference_in_hours_between(prev_ts) >= hours
})
}
fn compute_rolling_start_inner<F, D>(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
starting_height: Height,
exit: &Exit,
get_field: F,
expired: D,
@@ -323,7 +317,7 @@ impl Vecs {
D: Fn(Timestamp, Timestamp) -> bool,
{
let field = get_field(self);
let resume_from = field.len().min(starting_indexes.height.to_usize());
let resume_from = field.len().min(starting_height.to_usize());
let mut prev = if resume_from > 0 {
field.collect_one_at(resume_from - 1).unwrap()
} else {
@@ -333,7 +327,7 @@ impl Vecs {
cursor.advance(prev.to_usize());
let mut prev_ts = cursor.next().unwrap();
Ok(field.compute_transform(
starting_indexes.height,
starting_height,
&indexes.timestamp.monotonic,
|(h, t, ..)| {
while expired(t, prev_ts) {
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Indexes, StoredU64};
use brk_types::StoredU64;
use vecdb::Exit;
use super::Vecs;
@@ -11,25 +11,23 @@ impl Vecs {
&mut self,
indexer: &Indexer,
lookback: &blocks::LookbackVecs,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let window_starts = lookback.window_starts();
// vbytes = floor(weight / 4), stored at height level
self.vbytes
.compute(starting_indexes.height, &window_starts, exit, |height| {
.compute(starting_height, &window_starts, exit, |height| {
Ok(height.compute_transform(
starting_indexes.height,
starting_height,
&indexer.vecs.blocks.weight,
|(h, weight, ..)| (h, StoredU64::from(weight.to_vbytes_floor())),
exit,
)?)
})?;
// size from indexer total_size
self.size.compute(
starting_indexes.height,
starting_height,
&window_starts,
&indexer.vecs.blocks.total,
exit,
@@ -1,19 +1,15 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{BasisPoints16, Indexes};
use brk_types::BasisPoints16;
use vecdb::Exit;
use super::Vecs;
impl Vecs {
pub(crate) fn compute(
&mut self,
indexer: &Indexer,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
self.fullness.bps.compute_transform(
starting_indexes.height,
starting_height,
&indexer.vecs.blocks.weight,
|(h, weight, ..)| (h, BasisPoints16::from(weight.fullness())),
exit,
@@ -1,5 +1,6 @@
use brk_error::Result;
use brk_types::{Bitcoin, Indexes, StoredF64};
use brk_indexer::Indexer;
use brk_types::{Bitcoin, StoredF64};
use vecdb::Exit;
use super::Vecs;
@@ -8,17 +9,18 @@ use crate::distribution;
impl Vecs {
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
indexer: &Indexer,
distribution: &distribution::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let all_metrics = &distribution.utxo_cohorts.all.metrics;
let circulating_supply = &all_metrics.supply.total.sats.height;
self.coinblocks_created
.compute(starting_indexes.height, exit, |vec| {
.compute(starting_height, exit, |vec| {
vec.compute_transform(
starting_indexes.height,
starting_height,
circulating_supply,
|(i, v, ..)| (i, StoredF64::from(Bitcoin::from(v))),
exit,
@@ -27,9 +29,9 @@ impl Vecs {
})?;
self.coinblocks_stored
.compute(starting_indexes.height, exit, |vec| {
.compute(starting_height, exit, |vec| {
vec.compute_subtract(
starting_indexes.height,
starting_height,
&self.coinblocks_created.block,
&distribution.coinblocks_destroyed.block,
exit,
@@ -38,14 +40,14 @@ impl Vecs {
})?;
self.liveliness.height.compute_divide(
starting_indexes.height,
starting_height,
&distribution.coinblocks_destroyed.cumulative.height,
&self.coinblocks_created.cumulative.height,
exit,
)?;
self.ratio.height.compute_divide(
starting_indexes.height,
starting_height,
&self.liveliness.height,
&self.vaultedness.height,
exit,
@@ -1,5 +1,6 @@
use brk_error::Result;
use brk_types::{BasisPointsSigned32, Indexes};
use brk_indexer::Indexer;
use brk_types::BasisPointsSigned32;
use vecdb::Exit;
use super::super::activity;
@@ -9,13 +10,15 @@ use crate::supply;
impl Vecs {
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
indexer: &Indexer,
supply: &supply::Vecs,
activity: &activity::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
self.inflation_rate.bps.height.compute_transform2(
starting_indexes.height,
starting_height,
&activity.ratio.height,
&supply.inflation_rate.bps.height,
|(h, a2vr, inflation, ..)| {
@@ -28,14 +31,14 @@ impl Vecs {
)?;
self.tx_velocity_native.height.compute_multiply(
starting_indexes.height,
starting_height,
&activity.ratio.height,
&supply.velocity.native.height,
exit,
)?;
self.tx_velocity_fiat.height.compute_multiply(
starting_indexes.height,
starting_height,
&activity.ratio.height,
&supply.velocity.fiat.height,
exit,
@@ -1,5 +1,6 @@
use brk_error::Result;
use brk_types::{Dollars, Indexes};
use brk_indexer::Indexer;
use brk_types::Dollars;
use vecdb::Exit;
use super::super::{activity, value};
@@ -10,40 +11,41 @@ impl Vecs {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
indexer: &Indexer,
mining: &mining::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
value: &value::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
let all_metrics = &distribution.utxo_cohorts.all.metrics;
let realized_cap_cents = &all_metrics.realized.cap.cents.height;
let circulating_supply = &all_metrics.supply.total.btc.height;
self.thermo.cents.height.compute_transform(
starting_indexes.height,
starting_lengths.height,
&mining.rewards.subsidy.cumulative.cents.height,
|(i, v, ..)| (i, v),
exit,
)?;
self.investor.cents.height.compute_subtract(
starting_indexes.height,
starting_lengths.height,
realized_cap_cents,
&self.thermo.cents.height,
exit,
)?;
self.vaulted.cents.height.compute_multiply(
starting_indexes.height,
starting_lengths.height,
realized_cap_cents,
&activity.vaultedness.height,
exit,
)?;
self.active.cents.height.compute_multiply(
starting_indexes.height,
starting_lengths.height,
realized_cap_cents,
&activity.liveliness.height,
exit,
@@ -51,7 +53,7 @@ impl Vecs {
// cointime_cap = (cointime_value_destroyed_cumulative * circulating_supply) / coinblocks_stored_cumulative
self.cointime.cents.height.compute_transform3(
starting_indexes.height,
starting_lengths.height,
&value.destroyed.cumulative.height,
circulating_supply,
&activity.coinblocks_stored.cumulative.height,
@@ -67,7 +69,7 @@ impl Vecs {
// AVIV = active_cap / investor_cap
self.aviv.compute_ratio(
starting_indexes,
&starting_lengths,
&self.active.cents.height,
&self.investor.cents.height,
exit,
+12 -18
View File
@@ -1,16 +1,16 @@
use brk_error::Result;
use brk_types::Indexes;
use brk_indexer::Indexer;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, distribution, mining, prices, supply};
use crate::{blocks, distribution, mining, price, supply};
impl Vecs {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
prices: &prices::Vecs,
indexer: &Indexer,
prices: &price::Vecs,
blocks: &blocks::Vecs,
mining: &mining::Vecs,
supply_vecs: &supply::Vecs,
@@ -20,29 +20,23 @@ impl Vecs {
self.db.sync_bg_tasks()?;
// Activity computes first (liveliness, vaultedness, etc.)
self.activity
.compute(starting_indexes, distribution, exit)?;
self.activity.compute(indexer, distribution, exit)?;
// Phase 2: supply, adjusted, value are independent (all depend only on activity)
let (r1, r2) = rayon::join(
|| {
self.supply
.compute(starting_indexes, prices, distribution, &self.activity, exit)
.compute(indexer, prices, distribution, &self.activity, exit)
},
|| {
rayon::join(
|| {
self.adjusted
.compute(starting_indexes, supply_vecs, &self.activity, exit)
.compute(indexer, supply_vecs, &self.activity, exit)
},
|| {
self.value.compute(
starting_indexes,
prices,
distribution,
&self.activity,
exit,
)
self.value
.compute(indexer, prices, distribution, &self.activity, exit)
},
)
},
@@ -53,7 +47,7 @@ impl Vecs {
// Cap depends on activity + value
self.cap.compute(
starting_indexes,
indexer,
mining,
distribution,
&self.activity,
@@ -65,7 +59,7 @@ impl Vecs {
let (r3, r4) = rayon::join(
|| {
self.prices.compute(
starting_indexes,
indexer,
prices,
distribution,
&self.activity,
@@ -76,7 +70,7 @@ impl Vecs {
},
|| {
self.reserve_risk
.compute(starting_indexes, blocks, prices, &self.value, exit)
.compute(indexer, blocks, prices, &self.value, exit)
},
);
r3?;
@@ -1,31 +1,33 @@
use brk_error::Result;
use brk_types::{Cents, Indexes};
use brk_indexer::Indexer;
use brk_types::Cents;
use vecdb::Exit;
use super::super::{activity, cap, supply};
use super::Vecs;
use crate::{distribution, prices};
use crate::{distribution, price};
impl Vecs {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
prices: &prices::Vecs,
indexer: &Indexer,
prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
supply: &supply::Vecs,
cap: &cap::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
let all_metrics = &distribution.utxo_cohorts.all.metrics;
let circulating_supply = &all_metrics.supply.total.btc.height;
let realized_price = &all_metrics.realized.price.cents.height;
self.vaulted
.compute_all(prices, starting_indexes, exit, |v| {
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_indexes.height,
starting_lengths.height,
realized_price,
&activity.vaultedness.height,
|(i, price, vaultedness, ..)| {
@@ -36,9 +38,9 @@ impl Vecs {
})?;
self.active
.compute_all(prices, starting_indexes, exit, |v| {
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_indexes.height,
starting_lengths.height,
realized_price,
&activity.liveliness.height,
|(i, price, liveliness, ..)| {
@@ -49,9 +51,9 @@ impl Vecs {
})?;
self.true_market_mean
.compute_all(prices, starting_indexes, exit, |v| {
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_indexes.height,
starting_lengths.height,
&cap.investor.cents.height,
&supply.active.btc.height,
|(i, cap_cents, supply_btc, ..)| {
@@ -62,9 +64,9 @@ impl Vecs {
})?;
self.cointime
.compute_all(prices, starting_indexes, exit, |v| {
.compute_all(prices, &starting_lengths, exit, |v| {
Ok(v.compute_transform2(
starting_indexes.height,
starting_lengths.height,
&cap.cointime.cents.height,
circulating_supply,
|(i, cap_cents, supply_btc, ..)| {
@@ -1,28 +1,31 @@
use brk_error::Result;
use brk_types::{Indexes, StoredF64};
use brk_indexer::Indexer;
use brk_types::StoredF64;
use vecdb::Exit;
use super::{super::value, Vecs};
use crate::{blocks, internal::algo::ComputeRollingMedianFromStarts, prices};
use crate::{blocks, internal::algo::ComputeRollingMedianFromStarts, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
indexer: &Indexer,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
prices: &price::Vecs,
value: &value::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
self.vocdd_median_1y.compute_rolling_median_from_starts(
starting_indexes.height,
starting_height,
&blocks.lookback._1y,
&value.vocdd.block,
exit,
)?;
self.hodl_bank.compute_cumulative_transformed_binary(
starting_indexes.height,
starting_height,
&prices.spot.usd.height,
&self.vocdd_median_1y,
|price, median| StoredF64::from(f64::from(price) - f64::from(median)),
@@ -30,7 +33,7 @@ impl Vecs {
)?;
self.value.height.compute_divide(
starting_indexes.height,
starting_height,
&prices.spot.usd.height,
&self.hodl_bank,
exit,
@@ -1,20 +1,21 @@
use brk_error::Result;
use brk_types::Indexes;
use brk_indexer::Indexer;
use vecdb::Exit;
use super::super::activity;
use super::Vecs;
use crate::{distribution, prices};
use crate::{distribution, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
prices: &prices::Vecs,
indexer: &Indexer,
prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let circulating_supply = &distribution
.utxo_cohorts
.all
@@ -25,22 +26,21 @@ impl Vecs {
.height;
self.vaulted.sats.height.compute_multiply(
starting_indexes.height,
starting_height,
circulating_supply,
&activity.vaultedness.height,
exit,
)?;
self.active.sats.height.compute_multiply(
starting_indexes.height,
starting_height,
circulating_supply,
&activity.liveliness.height,
exit,
)?;
self.vaulted
.compute(prices, starting_indexes.height, exit)?;
self.active.compute(prices, starting_indexes.height, exit)?;
self.vaulted.compute(prices, starting_height, exit)?;
self.active.compute(prices, starting_height, exit)?;
Ok(())
}
@@ -1,39 +1,40 @@
use brk_error::Result;
use brk_types::{Bitcoin, Dollars, Indexes, StoredF64};
use brk_indexer::Indexer;
use brk_types::{Bitcoin, Dollars, StoredF64};
use vecdb::Exit;
use super::super::activity;
use super::Vecs;
use crate::{distribution, prices};
use crate::{distribution, price};
impl Vecs {
pub(crate) fn compute(
&mut self,
starting_indexes: &Indexes,
prices: &prices::Vecs,
indexer: &Indexer,
prices: &price::Vecs,
distribution: &distribution::Vecs,
activity: &activity::Vecs,
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let all_metrics = &distribution.utxo_cohorts.all.metrics;
let coinblocks_destroyed = &distribution.coinblocks_destroyed;
let coindays_destroyed = &all_metrics.activity.coindays_destroyed;
let circulating_supply = &all_metrics.supply.total.btc.height;
self.destroyed
.compute(starting_indexes.height, exit, |vec| {
vec.compute_multiply(
starting_indexes.height,
&prices.spot.usd.height,
&coinblocks_destroyed.block,
exit,
)?;
Ok(())
})?;
self.created.compute(starting_indexes.height, exit, |vec| {
self.destroyed.compute(starting_height, exit, |vec| {
vec.compute_multiply(
starting_indexes.height,
starting_height,
&prices.spot.usd.height,
&coinblocks_destroyed.block,
exit,
)?;
Ok(())
})?;
self.created.compute(starting_height, exit, |vec| {
vec.compute_multiply(
starting_height,
&prices.spot.usd.height,
&activity.coinblocks_created.block,
exit,
@@ -41,9 +42,9 @@ impl Vecs {
Ok(())
})?;
self.stored.compute(starting_indexes.height, exit, |vec| {
self.stored.compute(starting_height, exit, |vec| {
vec.compute_multiply(
starting_indexes.height,
starting_height,
&prices.spot.usd.height,
&activity.coinblocks_stored.block,
exit,
@@ -54,9 +55,9 @@ impl Vecs {
// VOCDD: Value of Coin Days Destroyed = price × (CDD / circulating_supply)
// Supply-adjusted to account for growing supply over time
// This is a key input for Reserve Risk / HODL Bank calculation
self.vocdd.compute(starting_indexes.height, exit, |vec| {
self.vocdd.compute(starting_height, exit, |vec| {
vec.compute_transform3(
starting_indexes.height,
starting_height,
&prices.spot.usd.height,
&coindays_destroyed.block,
circulating_supply,
@@ -1,6 +1,7 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Indexes, Version};
use brk_types::Version;
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
@@ -70,9 +71,9 @@ impl AddrCountFundedTotalVecs {
self.total.push_counts(total);
}
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)?;
pub(crate) fn compute_rest(&mut self, starting_lengths: &Lengths, exit: &Exit) -> Result<()> {
self.funded.compute_rest(starting_lengths, exit)?;
self.total.compute_rest(starting_lengths, exit)?;
Ok(())
}
}
@@ -35,8 +35,9 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, Version};
use brk_types::{Height, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
@@ -44,7 +45,7 @@ use super::{
count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
};
use crate::{indexes, prices};
use crate::{indexes, price};
mod state;
@@ -102,17 +103,17 @@ impl ExposedAddrVecs {
pub(crate) fn compute_rest(
&mut self,
starting_indexes: &Indexes,
prices: &prices::Vecs,
starting_lengths: &Lengths,
prices: &price::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.count.compute_rest(starting_lengths, exit)?;
self.supply
.compute_rest(starting_indexes.height, prices, exit)?;
.compute_rest(starting_lengths.height, prices, exit)?;
self.supply_share.compute_rest(
starting_indexes.height,
starting_lengths.height,
&self.supply,
all_supply_sats,
type_supply_sats,
@@ -1,7 +1,8 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Indexes, OutputType, StoredF32, StoredU32, StoredU64, Version};
use brk_types::{BasisPoints16, OutputType, StoredF32, StoredU32, StoredU64, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
@@ -205,37 +206,37 @@ impl AddrEventsVecs {
pub(crate) fn compute_rest(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
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)?;
.compute_rest(starting_lengths.height, exit)?;
self.input_from_reused_addr_count
.compute_rest(starting_indexes.height, exit)?;
.compute_rest(starting_lengths.height, exit)?;
self.active_reused_addr_count
.compute_rest(starting_indexes.height, exit)?;
.compute_rest(starting_lengths.height, exit)?;
self.active_reused_addr_share
.compute_rest(starting_indexes.height, exit)?;
.compute_rest(starting_lengths.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,
starting_lengths.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,
starting_lengths.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,
starting_lengths.height,
exit,
)?;
for otype in OutputType::ADDR_TYPES {
@@ -247,7 +248,7 @@ impl AddrEventsVecs {
.by_addr_type
.get_unwrap(otype),
outputs_by_type.output_count.by_type.get(otype),
starting_indexes.height,
starting_lengths.height,
exit,
)?;
self.input_from_reused_addr_share
@@ -258,7 +259,7 @@ impl AddrEventsVecs {
.by_addr_type
.get_unwrap(otype),
inputs_by_type.input_count.by_type.get(otype),
starting_indexes.height,
starting_lengths.height,
exit,
)?;
}
@@ -22,8 +22,9 @@ pub use events::{AddrEventsVecs, AddrTypeToAddrEventCount};
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, Version};
use brk_types::{Height, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
@@ -34,7 +35,7 @@ use super::{
use crate::{
indexes, inputs,
internal::{WindowStartVec, Windows},
outputs, prices,
outputs, price,
};
mod state;
@@ -108,21 +109,21 @@ impl ReusedAddrVecs {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
outputs_by_type: &outputs::ByTypeVecs,
inputs_by_type: &inputs::ByTypeVecs,
prices: &prices::Vecs,
prices: &price::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.count.compute_rest(starting_lengths, exit)?;
self.events
.compute_rest(starting_indexes, outputs_by_type, inputs_by_type, exit)?;
.compute_rest(starting_lengths, outputs_by_type, inputs_by_type, exit)?;
self.supply
.compute_rest(starting_indexes.height, prices, exit)?;
.compute_rest(starting_lengths.height, prices, exit)?;
self.supply_share.compute_rest(
starting_indexes.height,
starting_lengths.height,
&self.supply,
all_supply_sats,
type_supply_sats,
@@ -2,8 +2,9 @@ use std::path::Path;
use brk_cohort::{AddrGroups, AmountRange, Filter, Filtered, OverAmount, UnderAmount};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, StoredU64, Version};
use brk_types::{Height, Sats, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
@@ -12,7 +13,7 @@ use crate::{
distribution::DynCohortVecs,
indexes,
internal::{WindowStartVec, Windows},
prices,
price,
};
use super::{super::traits::CohortVecs, vecs::AddrCohortVecs};
@@ -83,23 +84,23 @@ impl AddrCohorts {
/// Compute overlapping cohorts from component amount_range cohorts.
pub(crate) fn compute_overlapping_vecs(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.for_each_aggregate(|vecs, sources| {
vecs.compute_from_stateful(starting_indexes, &sources, exit)
vecs.compute_from_stateful(starting_lengths, &sources, exit)
})
}
/// First phase of post-processing: compute index transforms.
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.par_iter_mut()
.try_for_each(|v| v.compute_rest_part1(prices, starting_indexes, exit))?;
.try_for_each(|v| v.compute_rest_part1(prices, starting_lengths, exit))?;
Ok(())
}
@@ -107,8 +108,8 @@ impl AddrCohorts {
/// Second phase of post-processing: compute relative metrics.
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
@@ -116,7 +117,7 @@ impl AddrCohorts {
self.0.par_iter_mut().try_for_each(|v| {
v.compute_rest_part2(
prices,
starting_indexes,
starting_lengths,
all_supply_sats,
all_utxo_count,
exit,
@@ -2,8 +2,9 @@ use std::path::Path;
use brk_cohort::{CohortContext, Filter, Filtered};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Cents, Height, Indexes, Sats, StoredI64, StoredU64, Version};
use brk_types::{BasisPointsSigned32, Cents, Height, Sats, StoredI64, StoredU64, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
@@ -11,7 +12,7 @@ use crate::{
distribution::state::{AddrCohortState, MinimalRealizedState},
indexes,
internal::{PerBlockWithDeltas, WindowStartVec, Windows},
prices,
price,
};
use crate::distribution::metrics::{ImportConfig, MinimalCohortMetrics};
@@ -173,12 +174,12 @@ impl DynCohortVecs for AddrCohortVecs {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_rest_part1(prices, starting_indexes, exit)
.compute_rest_part1(prices, starting_lengths, exit)
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
@@ -205,12 +206,12 @@ impl DynCohortVecs for AddrCohortVecs {
impl CohortVecs for AddrCohortVecs {
fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.addr_count.height.compute_sum_of_others(
starting_indexes.height,
starting_lengths.height,
others
.iter()
.map(|v| &v.addr_count.height)
@@ -219,7 +220,7 @@ impl CohortVecs for AddrCohortVecs {
exit,
)?;
self.metrics.compute_from_sources(
starting_indexes,
starting_lengths,
&others.iter().map(|v| &v.metrics).collect::<Vec<_>>(),
exit,
)?;
@@ -228,15 +229,15 @@ impl CohortVecs for AddrCohortVecs {
fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
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,
starting_lengths,
all_supply_sats,
all_utxo_count,
exit,
@@ -1,8 +1,9 @@
use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Sats, StoredU64, Version};
use brk_indexer::Lengths;
use brk_types::{Cents, Height, Sats, StoredU64, Version};
use vecdb::{Exit, ReadableVec};
use crate::prices;
use crate::price;
/// Dynamic dispatch trait for cohort vectors.
///
@@ -30,8 +31,8 @@ pub trait DynCohortVecs: Send + Sync {
/// First phase of post-processing computations.
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()>;
@@ -52,7 +53,7 @@ pub trait CohortVecs: DynCohortVecs {
/// Compute aggregate cohort from component cohorts.
fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()>;
@@ -60,8 +61,8 @@ pub trait CohortVecs: DynCohortVecs {
/// Second phase of post-processing computations.
fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
@@ -30,18 +30,34 @@ const TREE_SIZE: usize = TIER0_COUNT + TIER1_COUNT + OVERFLOW; // 190,001
pub(super) struct CostBasisNode {
all_sats: i64,
sth_sats: i64,
discount_sats: i64,
all_usd: i128,
sth_usd: i128,
discount_usd: i128,
}
impl CostBasisNode {
#[inline(always)]
fn new(sats: i64, usd: i128, is_sth: bool) -> Self {
fn new_supply(sats: i64, usd: i128, is_sth: bool) -> Self {
Self {
all_sats: sats,
sth_sats: if is_sth { sats } else { 0 },
discount_sats: 0,
all_usd: usd,
sth_usd: if is_sth { usd } else { 0 },
discount_usd: 0,
}
}
#[inline(always)]
fn new_discount(sats: i64, usd: i128) -> Self {
Self {
all_sats: 0,
sth_sats: 0,
discount_sats: sats,
all_usd: 0,
sth_usd: 0,
discount_usd: usd,
}
}
}
@@ -51,8 +67,10 @@ impl FenwickNode for CostBasisNode {
fn add_assign(&mut self, other: &Self) {
self.all_sats += other.all_sats;
self.sth_sats += other.sth_sats;
self.discount_sats += other.discount_sats;
self.all_usd += other.all_usd;
self.sth_usd += other.sth_usd;
self.discount_usd += other.discount_usd;
}
}
@@ -151,16 +169,34 @@ impl CostBasisFenwick {
}
let bucket = price_to_bucket(price);
let delta =
CostBasisNode::new(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
CostBasisNode::new_supply(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
self.tree.add(bucket, &delta);
self.totals.add_assign(&delta);
}
/// Bulk-initialize from BTreeMaps (one per age-range cohort).
/// Call after state import when all pending maps have been drained.
pub(super) fn bulk_init<'a>(
/// Apply a net delta from the discount-entry cohort.
///
/// Supply totals are maintained from the age-range cohorts; this updates
/// only the discount-entry partition so premium can be derived as all - discount.
pub(super) fn apply_discount_delta(&mut self, price: CentsCompact, pending: &PendingDelta) {
let net_sats = u64::from(pending.inc) as i64 - u64::from(pending.dec) as i64;
if net_sats == 0 {
return;
}
let bucket = price_to_bucket(price);
let delta =
CostBasisNode::new_discount(net_sats, price.as_u128() as i128 * net_sats as i128);
self.tree.add(bucket, &delta);
self.totals.add_assign(&delta);
}
/// Bulk-initialize from age-range maps plus the discount-entry map.
/// Age-range maps maintain all/STH/LTH totals; the discount-entry map
/// maintains only the discount partition used to derive premium.
pub(super) fn bulk_init_with_discount<'a>(
&mut self,
maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>,
discount_maps: impl Iterator<Item = &'a std::collections::BTreeMap<CentsCompact, Sats>>,
) {
self.tree.reset();
self.totals = CostBasisNode::default();
@@ -169,7 +205,18 @@ impl CostBasisFenwick {
for (&price, &sats) in map.iter() {
let bucket = price_to_bucket(price);
let s = u64::from(sats) as i64;
let node = CostBasisNode::new(s, price.as_u128() as i128 * s as i128, is_sth);
let node =
CostBasisNode::new_supply(s, price.as_u128() as i128 * s as i128, is_sth);
self.tree.add_raw(bucket, &node);
self.totals.add_assign(&node);
}
}
for map in discount_maps {
for (&price, &sats) in map.iter() {
let bucket = price_to_bucket(price);
let s = u64::from(sats) as i64;
let node = CostBasisNode::new_discount(s, price.as_u128() as i128 * s as i128);
self.tree.add_raw(bucket, &node);
self.totals.add_assign(&node);
}
@@ -212,6 +259,26 @@ impl CostBasisFenwick {
)
}
/// Compute percentile prices for discount-entry cohort.
pub(super) fn percentiles_discount_entry(&self) -> PercentileResult {
self.compute_percentiles(
self.totals.discount_sats,
self.totals.discount_usd,
|n| n.discount_sats,
|n| n.discount_usd,
)
}
/// Compute percentile prices for premium-entry cohort (all - discount).
pub(super) fn percentiles_premium_entry(&self) -> PercentileResult {
self.compute_percentiles(
self.totals.all_sats - self.totals.discount_sats,
self.totals.all_usd - self.totals.discount_usd,
|n| n.all_sats - n.discount_sats,
|n| n.all_usd - n.discount_usd,
)
}
fn compute_percentiles(
&self,
total_sats: i64,
@@ -271,6 +338,37 @@ impl CostBasisFenwick {
return (0, 0, 0);
}
let range = self.density_range(spot_price);
let all_range = range.all_sats.max(0);
let sth_range = range.sth_sats.max(0);
let lth_range = all_range - sth_range;
let lth_total = self.totals.all_sats - self.totals.sth_sats;
(
Self::to_bps(all_range, self.totals.all_sats),
Self::to_bps(sth_range, self.totals.sth_sats),
Self::to_bps(lth_range, lth_total),
)
}
/// Compute supply density for entry cohorts: (discount_bps, premium_bps).
pub(super) fn entry_density(&self, spot_price: Cents) -> (u16, u16) {
if self.totals.all_sats <= 0 {
return (0, 0);
}
let range = self.density_range(spot_price);
let discount_range = range.discount_sats.max(0);
let premium_range = range.all_sats.max(0) - discount_range;
let premium_total = self.totals.all_sats - self.totals.discount_sats;
(
Self::to_bps(discount_range, self.totals.discount_sats),
Self::to_bps(premium_range, premium_total),
)
}
fn density_range(&self, spot_price: Cents) -> CostBasisNode {
let spot_f64 = u64::from(spot_price) as f64;
let low = Cents::from((spot_f64 * 0.95) as u64);
let high = Cents::from((spot_f64 * 1.05) as u64);
@@ -285,24 +383,23 @@ impl CostBasisFenwick {
CostBasisNode::default()
};
let all_range = (cum_high.all_sats - cum_low.all_sats).max(0);
let sth_range = (cum_high.sth_sats - cum_low.sth_sats).max(0);
let lth_range = all_range - sth_range;
CostBasisNode {
all_sats: cum_high.all_sats - cum_low.all_sats,
sth_sats: cum_high.sth_sats - cum_low.sth_sats,
discount_sats: cum_high.discount_sats - cum_low.discount_sats,
all_usd: cum_high.all_usd - cum_low.all_usd,
sth_usd: cum_high.sth_usd - cum_low.sth_usd,
discount_usd: cum_high.discount_usd - cum_low.discount_usd,
}
}
let to_bps = |range: i64, total: i64| -> u16 {
if total <= 0 {
0
} else {
(range as f64 / total as f64 * 10000.0).round() as u16
}
};
let lth_total = self.totals.all_sats - self.totals.sth_sats;
(
to_bps(all_range, self.totals.all_sats),
to_bps(sth_range, self.totals.sth_sats),
to_bps(lth_range, lth_total),
)
#[inline(always)]
fn to_bps(range: i64, total: i64) -> u16 {
if total <= 0 {
0
} else {
(range as f64 / total as f64 * 10000.0).round() as u16
}
}
// -----------------------------------------------------------------------
@@ -1,12 +1,13 @@
use std::path::Path;
use brk_cohort::{
AgeRange, AmountRange, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge, OverAmount,
SpendableType, Term, UnderAge, UnderAmount,
AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge,
OverAmount, SpendableType, Term, UnderAge, UnderAmount,
};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, CentsSquaredSats, Dollars, Height, Indexes, Sats, Version};
use brk_types::{Cents, CentsSquaredSats, Dollars, Height, Sats, Version};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode, WritableVec,
@@ -15,7 +16,6 @@ use vecdb::{
use crate::{
blocks,
distribution::{
DynCohortVecs,
metrics::{
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
@@ -23,10 +23,11 @@ use crate::{
TypeCohortMetrics,
},
state::UTXOCohortState,
DynCohortVecs,
},
indexes,
internal::{ValuePerBlockCumulativeRolling, WindowStartVec, Windows},
prices,
price,
};
use super::{fenwick::CostBasisFenwick, vecs::UTXOCohortVecs};
@@ -44,6 +45,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub over_age: OverAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub class: Class<UTXOCohortVecs<CoreCohortMetrics<M>>>,
pub entry: ByEntry<UTXOCohortVecs<ExtendedCohortMetrics<M>>>,
pub over_amount: OverAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub amount_range: AmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub under_amount: UnderAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
@@ -66,8 +68,10 @@ pub(crate) struct UTXOCohortsTransientState {
}
impl UTXOCohorts<Rw> {
/// ~71 separate cohorts (21 age + 5 epoch + 18 class + 15 amount + 12 type)
const SEPARATE_COHORT_CAPACITY: usize = 80;
/// Separate cohorts currently total 72:
/// 21 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
/// Keep small headroom because this is only Vec allocation capacity.
const SEPARATE_COHORT_CAPACITY: usize = 82;
/// Import all UTXO cohorts from database.
pub(crate) fn forced_import(
@@ -135,6 +139,26 @@ impl UTXOCohorts<Rw> {
let epoch = ByEpoch::try_new(&core_separate)?;
let class = Class::try_new(&core_separate)?;
let extended_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<ExtendedCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
filter: &f,
full_name: &full_name,
version: v,
indexes,
cached_starts,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
state,
ExtendedCohortMetrics::forced_import(&cfg)?,
))
};
let entry = ByEntry::try_new(&extended_separate)?;
// Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState
let minimal_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<MinimalCohortMetrics>> {
@@ -280,6 +304,7 @@ impl UTXOCohorts<Rw> {
lth,
epoch,
class,
entry,
type_,
under_age,
over_age,
@@ -308,6 +333,7 @@ impl UTXOCohorts<Rw> {
sth,
caches,
age_range,
entry,
..
} = self;
caches
@@ -326,7 +352,15 @@ impl UTXOCohorts<Rw> {
Some((map, caches.fenwick.is_sth_at(i)))
})
.collect();
caches.fenwick.bulk_init(maps.into_iter());
let discount_maps = entry
.discount
.state
.as_ref()
.map(|state| state.cost_basis_map())
.into_iter();
caches
.fenwick
.bulk_init_with_discount(maps.into_iter(), discount_maps);
}
/// Apply pending deltas from all age-range cohorts to the Fenwick tree.
@@ -337,7 +371,10 @@ impl UTXOCohorts<Rw> {
}
// Destructure to get separate borrows on caches and age_range
let Self {
caches, age_range, ..
caches,
age_range,
entry,
..
} = self;
for (i, sub) in age_range.iter().enumerate() {
if let Some(state) = sub.state.as_ref() {
@@ -347,6 +384,11 @@ impl UTXOCohorts<Rw> {
});
}
}
if let Some(state) = entry.discount.state.as_ref() {
state.for_each_cost_basis_pending(|&price, delta| {
caches.fenwick.apply_discount_delta(price, delta);
});
}
}
/// Push maturation sats to the matured vecs for the given height.
@@ -364,6 +406,7 @@ impl UTXOCohorts<Rw> {
age_range,
epoch,
class,
entry,
amount_range,
type_,
..
@@ -373,6 +416,7 @@ impl UTXOCohorts<Rw> {
.map(|x| x as &mut dyn DynCohortVecs)
.chain(epoch.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(class.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(entry.par_iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(
amount_range
.par_iter_mut()
@@ -388,6 +432,7 @@ impl UTXOCohorts<Rw> {
age_range,
epoch,
class,
entry,
amount_range,
type_,
..
@@ -397,6 +442,7 @@ impl UTXOCohorts<Rw> {
.map(|x| x as &mut dyn DynCohortVecs)
.chain(epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(class.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(entry.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(amount_range.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
.chain(type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs))
}
@@ -408,13 +454,14 @@ impl UTXOCohorts<Rw> {
.map(|x| x as &dyn DynCohortVecs)
.chain(self.epoch.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.class.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.entry.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.amount_range.iter().map(|x| x as &dyn DynCohortVecs))
.chain(self.type_.iter().map(|x| x as &dyn DynCohortVecs))
}
pub(crate) fn compute_overlapping_vecs(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
let Self {
@@ -432,7 +479,7 @@ impl UTXOCohorts<Rw> {
let ar = &*age_range;
let amr = &*amount_range;
let si = starting_indexes;
let si = starting_lengths;
let tasks: Vec<Box<dyn FnOnce() -> Result<()> + Send + '_>> = vec![
Box::new(|| {
@@ -482,8 +529,8 @@ impl UTXOCohorts<Rw> {
/// First phase of post-processing: compute index transforms.
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
// 1. Compute all metrics except net_sentiment (all cohorts via DynCohortVecs)
@@ -515,6 +562,7 @@ impl UTXOCohorts<Rw> {
);
all.extend(self.epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.extend(self.class.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.extend(self.entry.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.extend(
self.amount_range
.iter_mut()
@@ -527,16 +575,16 @@ impl UTXOCohorts<Rw> {
);
all.extend(self.type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs));
all.into_par_iter()
.try_for_each(|v| v.compute_rest_part1(prices, starting_indexes, exit))?;
.try_for_each(|v| v.compute_rest_part1(prices, starting_lengths, exit))?;
}
// Compute matured cumulative + cents from sats × price
self.matured
.par_iter_mut()
.try_for_each(|v| v.compute_rest(starting_indexes.height, prices, exit))?;
.try_for_each(|v| v.compute_rest(starting_lengths.height, prices, exit))?;
// Compute profitability supply cents and realized price
self.profitability.compute(prices, starting_indexes, exit)?;
self.profitability.compute(prices, starting_lengths, exit)?;
Ok(())
}
@@ -545,8 +593,8 @@ impl UTXOCohorts<Rw> {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
@@ -574,7 +622,7 @@ impl UTXOCohorts<Rw> {
self.all.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
starting_lengths,
height_to_market_cap,
&under_1h_value_created,
&under_1h_value_destroyed,
@@ -603,6 +651,7 @@ impl UTXOCohorts<Rw> {
under_amount,
epoch,
class,
entry,
type_,
..
} = self;
@@ -619,7 +668,7 @@ impl UTXOCohorts<Rw> {
sth.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
starting_lengths,
height_to_market_cap,
vc,
vd,
@@ -632,7 +681,7 @@ impl UTXOCohorts<Rw> {
lth.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
starting_lengths,
height_to_market_cap,
ss,
au,
@@ -642,55 +691,68 @@ impl UTXOCohorts<Rw> {
Box::new(|| {
age_range.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
under_age.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
over_age.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
over_amount.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
epoch.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
class.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
entry.par_iter_mut().try_for_each(|v| {
v.metrics.compute_rest_part2(
blocks,
prices,
starting_lengths,
height_to_market_cap,
ss,
au,
exit,
)
})
}),
Box::new(|| {
amount_range.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
under_amount.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
Box::new(|| {
type_.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
.compute_rest_part2(prices, starting_lengths, ss, au, exit)
})
}),
];
@@ -729,6 +791,9 @@ impl UTXOCohorts<Rw> {
for v in self.class.iter_mut() {
vecs.extend(v.metrics.collect_all_vecs_mut());
}
for v in self.entry.iter_mut() {
vecs.extend(v.metrics.collect_all_vecs_mut());
}
for v in self.amount_range.iter_mut() {
vecs.extend(v.metrics.collect_all_vecs_mut());
}
@@ -812,7 +877,7 @@ impl UTXOCohorts<Rw> {
/// Aggregate RealizedFull fields from age_range states and push to all/sth/lth.
/// Called during the block loop after separate cohorts' push_state but before reset.
pub(crate) fn push_overlapping(&mut self, height_price: Cents) {
pub(crate) fn push_overlapping(&mut self, height_price: Cents) -> Cents {
let Self {
all,
sth,
@@ -851,7 +916,7 @@ impl UTXOCohorts<Rw> {
}
}
all.metrics.realized.push_accum(&all_acc);
let all_capitalized_price = all.metrics.realized.push_accum(&all_acc);
sth.metrics.realized.push_accum(&sth_acc);
lth.metrics.realized.push_accum(&lth_acc);
@@ -879,6 +944,8 @@ impl UTXOCohorts<Rw> {
.unrealized
.capitalized_cap_in_loss_raw
.push(CentsSquaredSats::new(lth_ccap.1));
all_capitalized_price
}
}
@@ -50,6 +50,22 @@ impl UTXOCohorts {
let lth = self.caches.fenwick.percentiles_lth();
push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis);
let (discount_d, premium_d) = self.caches.fenwick.entry_density(spot_price);
let discount = self.caches.fenwick.percentiles_discount_entry();
push_cost_basis(
&discount,
discount_d,
&mut self.entry.discount.metrics.cost_basis,
);
let premium = self.caches.fenwick.percentiles_premium_entry();
push_cost_basis(
&premium,
premium_d,
&mut self.entry.premium.metrics.cost_basis,
);
let prof = self.caches.fenwick.profitability(spot_price);
push_profitability(&prof, &mut self.profitability);
}
@@ -1,3 +1,4 @@
use brk_cohort::EntryPrice;
use brk_types::{Cents, CostBasisSnapshot, Height, Timestamp};
use vecdb::Rw;
@@ -12,6 +13,7 @@ impl UTXOCohorts<Rw> {
/// - The "under_1h" age cohort (all new UTXOs start at 0 hours old)
/// - The appropriate epoch cohort based on block height
/// - The appropriate class cohort based on block timestamp
/// - The immutable entry valuation cohort based on creation price versus anchor
/// - The appropriate output type cohort (P2PKH, P2SH, etc.)
/// - The appropriate amount range cohort based on value
pub(crate) fn receive(
@@ -20,13 +22,14 @@ impl UTXOCohorts<Rw> {
height: Height,
timestamp: Timestamp,
price: Cents,
entry: EntryPrice,
) {
let supply_state = received.spendable_supply;
// Pre-compute snapshot once for the 3 cohorts sharing the same supply_state
// Pre-compute snapshot once for cohorts sharing the block-level supply_state
let snapshot = CostBasisSnapshot::from_utxo(price, &supply_state);
// New UTXOs go into under_1h, current epoch, and current class
// New UTXOs go into under_1h plus immutable creation cohorts
self.age_range
.under_1h
.state
@@ -45,6 +48,12 @@ impl UTXOCohorts<Rw> {
.unwrap()
.receive_utxo_snapshot(&supply_state, &snapshot);
}
self.entry
.get_mut(entry)
.state
.as_mut()
.unwrap()
.receive_utxo_snapshot(&supply_state, &snapshot);
// Update output type cohorts (skip types with no outputs this block)
self.type_.iter_typed_mut().for_each(|(output_type, vecs)| {
@@ -49,7 +49,7 @@ impl UTXOCohorts<Rw> {
// This is the max price between receive and send heights
let peak_price = price_range_max.max_between(receive_height, send_height);
// Pre-compute once for age_range, epoch, year (all share sent.spendable_supply)
// Pre-compute once for cohorts sharing the sent supply.
if let Some(pre) = SendPrecomputed::new(
&sent.spendable_supply,
current_price,
@@ -75,6 +75,12 @@ impl UTXOCohorts<Rw> {
.unwrap()
.send_utxo_precomputed(&sent.spendable_supply, &pre);
}
self.entry
.get_mut(block_state.entry)
.state
.as_mut()
.unwrap()
.send_utxo_precomputed(&sent.spendable_supply, &pre);
} else if sent.spendable_supply.utxo_count > 0 {
// Zero-value UTXOs: just subtract supply
self.age_range.get_mut(age).state.as_mut().unwrap().supply -=
@@ -85,6 +91,12 @@ impl UTXOCohorts<Rw> {
if let Some(v) = self.class.mut_vec_from_timestamp(block_state.timestamp) {
v.state.as_mut().unwrap().supply -= &sent.spendable_supply;
}
self.entry
.get_mut(block_state.entry)
.state
.as_mut()
.unwrap()
.supply -= &sent.spendable_supply;
}
// Update output type cohorts (skip zero-supply entries)
@@ -1,11 +1,12 @@
use brk_cohort::{Filter, Filtered};
use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Version};
use brk_indexer::Lengths;
use brk_types::{Cents, Height, Version};
use vecdb::{Exit, ReadableVec};
use crate::{
distribution::{cohorts::traits::DynCohortVecs, metrics::CoreCohortMetrics},
prices,
price,
};
use super::UTXOCohortVecs;
@@ -55,12 +56,12 @@ impl DynCohortVecs for UTXOCohortVecs<CoreCohortMetrics> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_rest_part1(prices, starting_indexes, exit)
.compute_rest_part1(prices, starting_lengths, exit)
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
@@ -1,11 +1,12 @@
use brk_cohort::{Filter, Filtered};
use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Version};
use brk_indexer::Lengths;
use brk_types::{Cents, Height, Version};
use vecdb::{Exit, ReadableVec};
use crate::{
distribution::{cohorts::traits::DynCohortVecs, metrics::MinimalCohortMetrics},
prices,
price,
};
use super::UTXOCohortVecs;
@@ -48,12 +49,12 @@ impl DynCohortVecs for UTXOCohortVecs<MinimalCohortMetrics> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_rest_part1(prices, starting_indexes, exit)
.compute_rest_part1(prices, starting_lengths, exit)
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
@@ -44,8 +44,9 @@ mod r#type;
use brk_cohort::{Filter, Filtered};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Indexes, Version};
use brk_types::{Cents, Height, Version};
use vecdb::{Exit, ReadableVec};
use crate::{
@@ -54,7 +55,7 @@ use crate::{
metrics::{CohortMetricsBase, CohortMetricsState},
state::UTXOCohortState,
},
prices,
price,
};
#[derive(Traversable)]
@@ -185,12 +186,12 @@ impl<M: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<M> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_rest_part1(prices, starting_indexes, exit)?;
.compute_rest_part1(prices, starting_lengths, exit)?;
Ok(())
}
@@ -1,10 +1,11 @@
use brk_cohort::{Filter, Filtered};
use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Version};
use brk_indexer::Lengths;
use brk_types::{Cents, Height, Version};
use vecdb::{Exit, ReadableVec};
use crate::{
distribution::cohorts::traits::DynCohortVecs, distribution::metrics::TypeCohortMetrics, prices,
distribution::cohorts::traits::DynCohortVecs, distribution::metrics::TypeCohortMetrics, price,
};
use super::UTXOCohortVecs;
@@ -54,12 +55,12 @@ impl DynCohortVecs for UTXOCohortVecs<TypeCohortMetrics> {
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_rest_part1(prices, starting_indexes, exit)
.compute_rest_part1(prices, starting_lengths, exit)
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
@@ -1,4 +1,4 @@
use brk_cohort::ByAddrType;
use brk_cohort::{ByAddrType, EntryPrice};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{
@@ -46,6 +46,7 @@ pub(crate) fn process_blocks(
last_height: Height,
chain_state: &mut Vec<BlockState>,
tx_index_to_height: &mut RangeMap<TxIndex, Height>,
mut entry_anchor: Cents,
cached_prices: &[Cents],
cached_timestamps: &[Timestamp],
cached_price_range_max: &PriceRangeMax,
@@ -370,9 +371,14 @@ pub(crate) fn process_blocks(
.iterate(Sats::FIFTY_BTC, OutputType::P2PK65);
}
let entry = EntryPrice::from_is_discount(
entry_anchor == Cents::ZERO || block_price <= entry_anchor,
);
// Push current block state before processing cohort updates
chain_state.push(BlockState {
supply: transacted.spendable_supply,
entry,
price: block_price,
timestamp,
});
@@ -411,7 +417,7 @@ pub(crate) fn process_blocks(
|| {
// UTXO cohorts receive/send
vecs.utxo_cohorts
.receive(transacted, height, timestamp, block_price);
.receive(transacted, height, timestamp, block_price, entry);
if let Some(min_h) =
vecs.utxo_cohorts
.send(height_to_sent, chain_state, ctx.price_range_max)
@@ -460,7 +466,7 @@ pub(crate) fn process_blocks(
let is_last_of_day = is_last_of_day[offset];
let date_opt = is_last_of_day.then(|| Date::from(timestamp));
push_cohort_states(
entry_anchor = push_cohort_states(
&mut vecs.utxo_cohorts,
&mut vecs.addr_cohorts,
height,
@@ -527,7 +533,7 @@ fn push_cohort_states(
addr_cohorts: &mut AddrCohorts,
height: Height,
height_price: Cents,
) {
) -> Cents {
// Phase 1: push + unrealized (no reset yet, states still needed for aggregation)
rayon::join(
|| {
@@ -545,7 +551,7 @@ fn push_cohort_states(
);
// Phase 2: aggregate age_range states → push to overlapping cohorts
utxo_cohorts.push_overlapping(height_price);
let all_capitalized_price = utxo_cohorts.push_overlapping(height_price);
// Phase 3: reset per-block values
utxo_cohorts
@@ -554,4 +560,6 @@ fn push_cohort_states(
addr_cohorts
.iter_separate_mut()
.for_each(|v| v.reset_single_iteration_values());
all_capitalized_price
}
@@ -24,51 +24,60 @@ pub struct RecoveredState {
/// Returns Height::ZERO if any validation fails (triggers fresh start).
pub(crate) fn recover_state(
height: Height,
chain_state_rollback: vecdb::Result<Stamp>,
chain_state_rollback: Option<vecdb::Result<Stamp>>,
any_addr_indexes: &mut AnyAddrIndexesVecs,
addrs_data: &mut AddrsDataVecs,
utxo_cohorts: &mut UTXOCohorts,
addr_cohorts: &mut AddrCohorts,
) -> Result<RecoveredState> {
let stamp = Stamp::from(height);
// `None`: clean resume, already at the checkpoint, nothing to undo.
// `Some`: reorg, undo state past the resume point.
let consistent_height = match chain_state_rollback {
None => height,
Some(chain_state_rollback) => {
let stamp = Stamp::from(height);
// Rollback address state vectors
let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp);
let addr_data_rollback = addrs_data.rollback_before(stamp);
// Rollback address state vectors
let addr_indexes_rollback = any_addr_indexes.rollback_before(stamp);
let addr_data_rollback = addrs_data.rollback_before(stamp);
// Verify rollback consistency - all must agree on the same height
let consistent_height = rollback_states(
chain_state_rollback,
addr_indexes_rollback,
addr_data_rollback,
);
// Verify rollback consistency - all must agree on the same height
let consistent_height = rollback_states(
chain_state_rollback,
addr_indexes_rollback,
addr_data_rollback,
);
// If rollbacks are inconsistent, start fresh
if consistent_height.is_zero() {
warn!("Rollback consistency check failed: inconsistent heights");
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
// If rollbacks are inconsistent, start fresh
if consistent_height.is_zero() {
warn!("Rollback consistency check failed: inconsistent heights");
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
// Rollback can land at an earlier height (multi-block change file), which is fine.
// But if it lands AHEAD of target, that means rollback failed (missing change files).
if consistent_height > height {
warn!(
"Rollback failed: still at {} but target was {}, falling back to fresh start",
consistent_height, height
);
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
// Rollback can land at an earlier height (multi-block change file), which is fine.
// But if it lands AHEAD of target, that means rollback failed (missing change files).
if consistent_height > height {
warn!(
"Rollback failed: still at {} but target was {}, falling back to fresh start",
consistent_height, height
);
return Ok(RecoveredState {
starting_height: Height::ZERO,
});
}
if consistent_height != height {
debug!(
"Rollback landed at {} instead of {}, will resume from there",
consistent_height, height
);
}
if consistent_height != height {
debug!(
"Rollback landed at {} instead of {}, will resume from there",
consistent_height, height
);
}
consistent_height
}
};
// Import UTXO cohort states - all must succeed
debug!(
@@ -1,6 +1,7 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Indexes, StoredF64, Version};
use brk_types::{Bitcoin, StoredF64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
@@ -10,7 +11,7 @@ use crate::{
state::{CohortState, CostBasisOps, RealizedOps},
},
internal::{PerBlockCumulativeRolling, ValuePerBlockCumulativeRolling},
prices,
price,
};
use super::ActivityMinimal;
@@ -80,35 +81,35 @@ impl ActivityCore {
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
let minimal_refs: Vec<&ActivityMinimal> = others.iter().map(|o| &o.minimal).collect();
self.minimal
.compute_from_stateful(starting_indexes, &minimal_refs, exit)?;
.compute_from_stateful(starting_lengths, &minimal_refs, exit)?;
sum_others!(self, starting_indexes, others, exit; coindays_destroyed.block);
sum_others!(self, starting_indexes, others, exit; transfer_volume_in_profit.block.sats);
sum_others!(self, starting_indexes, others, exit; transfer_volume_in_loss.block.sats);
sum_others!(self, starting_lengths, others, exit; coindays_destroyed.block);
sum_others!(self, starting_lengths, others, exit; transfer_volume_in_profit.block.sats);
sum_others!(self, starting_lengths, others, exit; transfer_volume_in_loss.block.sats);
Ok(())
}
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.minimal
.compute_rest_part1(prices, starting_indexes, exit)?;
.compute_rest_part1(prices, starting_lengths, exit)?;
self.coindays_destroyed
.compute_rest(starting_indexes.height, exit)?;
.compute_rest(starting_lengths.height, exit)?;
self.transfer_volume_in_profit
.compute_rest(starting_indexes.height, prices, exit)?;
.compute_rest(starting_lengths.height, prices, exit)?;
self.transfer_volume_in_loss
.compute_rest(starting_indexes.height, prices, exit)?;
.compute_rest(starting_lengths.height, prices, exit)?;
Ok(())
}
}
@@ -1,6 +1,7 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Indexes, StoredF32, StoredF64, Version};
use brk_types::{StoredF32, StoredF64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
@@ -11,7 +12,7 @@ use crate::{
metrics::ImportConfig,
state::{CohortState, CostBasisOps, RealizedOps},
},
prices,
price,
};
use super::ActivityCore;
@@ -78,22 +79,22 @@ impl ActivityFull {
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&ActivityCore],
exit: &Exit,
) -> Result<()> {
self.inner
.compute_from_stateful(starting_indexes, others, exit)
.compute_from_stateful(starting_lengths, others, exit)
}
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.inner
.compute_rest_part1(prices, starting_indexes, exit)?;
.compute_rest_part1(prices, starting_lengths, exit)?;
for ((dormancy, cdd_sum), tv_sum) in self
.dormancy
@@ -103,7 +104,7 @@ impl ActivityFull {
.zip(self.inner.minimal.transfer_volume.sum.0.as_array())
{
dormancy.height.compute_transform2(
starting_indexes.height,
starting_lengths.height,
&cdd_sum.height,
&tv_sum.btc.height,
|(i, rolling_cdd, rolling_btc, ..)| {
@@ -1,6 +1,7 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Indexes, Version};
use brk_types::Version;
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{
@@ -9,7 +10,7 @@ use crate::{
state::{CohortState, CostBasisOps, RealizedOps},
},
internal::ValuePerBlockCumulativeRolling,
prices,
price,
};
#[derive(Traversable)]
@@ -44,12 +45,12 @@ impl ActivityMinimal {
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.transfer_volume.block.sats.compute_sum_of_others(
starting_indexes.height,
starting_lengths.height,
&others
.iter()
.map(|v| &v.transfer_volume.block.sats)
@@ -62,12 +63,12 @@ impl ActivityMinimal {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.transfer_volume
.compute_rest(starting_indexes.height, prices, exit)?;
.compute_rest(starting_lengths.height, prices, exit)?;
Ok(())
}
}
@@ -7,12 +7,13 @@ pub use full::ActivityFull;
pub use minimal::ActivityMinimal;
use brk_error::Result;
use brk_types::{Indexes, Version};
use brk_indexer::Lengths;
use brk_types::Version;
use vecdb::Exit;
use crate::{
distribution::state::{CohortState, CostBasisOps, RealizedOps},
prices,
price,
};
pub trait ActivityLike: Send + Sync {
@@ -23,14 +24,14 @@ pub trait ActivityLike: Send + Sync {
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>;
fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&ActivityCore],
exit: &Exit,
) -> Result<()>;
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()>;
}
@@ -53,19 +54,19 @@ impl ActivityLike for ActivityCore {
}
fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&ActivityCore],
exit: &Exit,
) -> Result<()> {
self.compute_from_stateful(starting_indexes, others, exit)
self.compute_from_stateful(starting_lengths, others, exit)
}
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.compute_rest_part1(prices, starting_indexes, exit)
self.compute_rest_part1(prices, starting_lengths, exit)
}
}
@@ -87,18 +88,18 @@ impl ActivityLike for ActivityFull {
}
fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&ActivityCore],
exit: &Exit,
) -> Result<()> {
self.compute_from_stateful(starting_indexes, others, exit)
self.compute_from_stateful(starting_lengths, others, exit)
}
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.compute_rest_part1(prices, starting_indexes, exit)
self.compute_rest_part1(prices, starting_lengths, exit)
}
}
@@ -1,7 +1,8 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Indexes, Version};
use brk_types::{Cents, Dollars, Height, Version};
use vecdb::{AnyStoredVec, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode};
use crate::{
@@ -10,7 +11,7 @@ use crate::{
ActivityFull, AdjustedSopr, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase,
RealizedFull, RelativeForAll, SupplyCore, UnrealizedFull,
},
prices,
price,
};
/// All-cohort metrics: extended realized + adjusted (as composable add-on),
@@ -99,8 +100,8 @@ impl AllCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
under_1h_value_created: &impl ReadableVec<Height, Cents>,
under_1h_value_destroyed: &impl ReadableVec<Height, Cents>,
@@ -109,7 +110,7 @@ impl AllCohortMetrics {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
starting_lengths,
&self.supply.total.btc.height,
height_to_market_cap,
&self.activity.transfer_volume,
@@ -117,14 +118,14 @@ impl AllCohortMetrics {
)?;
self.unrealized.compute(
starting_indexes.height,
starting_lengths.height,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.asopr.compute_rest_part2(
starting_indexes,
starting_lengths,
&self.activity.transfer_volume.block.cents,
&self.realized.core.sopr.value_destroyed.block,
under_1h_value_created,
@@ -134,10 +135,10 @@ impl AllCohortMetrics {
let all_utxo_count = self.outputs.unspent_count.height.read_only_clone();
self.outputs
.compute_part2(starting_indexes.height, &all_utxo_count, exit)?;
.compute_part2(starting_lengths.height, &all_utxo_count, exit)?;
self.cost_basis.compute_prices(
starting_indexes,
starting_lengths,
&prices.spot.cents.height,
&self.unrealized.invested_capital.in_profit.cents.height,
&self.unrealized.invested_capital.in_loss.cents.height,
@@ -149,14 +150,14 @@ impl AllCohortMetrics {
)?;
self.unrealized
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
.compute_sentiment(starting_lengths, &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)?;
.compute_dominance(starting_lengths.height, &own_supply_sats, exit)?;
self.relative.compute(
starting_indexes.height,
starting_lengths.height,
&self.supply,
&self.unrealized,
&self.realized,
@@ -1,7 +1,8 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, StoredU64};
use brk_types::{Height, Sats, StoredU64};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
@@ -9,7 +10,7 @@ use crate::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore,
},
prices,
price,
};
/// Basic cohort metrics: no extensions, used by age_range cohorts.
@@ -60,32 +61,32 @@ impl BasicCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
prices,
starting_indexes,
starting_lengths,
&self.supply.total.btc.height,
&self.activity.transfer_volume.sum._24h.cents.height,
exit,
)?;
self.unrealized.compute(
starting_indexes.height,
starting_lengths.height,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
.compute_dominance(starting_lengths.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
.compute_part2(starting_lengths.height, all_utxo_count, exit)?;
Ok(())
}
@@ -1,7 +1,8 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, StoredU64, Version};
use brk_types::{Height, Sats, StoredU64, Version};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
@@ -9,7 +10,7 @@ use crate::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore,
},
prices,
price,
};
#[derive(Traversable)]
@@ -63,32 +64,32 @@ impl CoreCohortMetrics {
/// Aggregate Core-tier fields from CohortMetricsBase sources (e.g. age_range -> under_age/over_age).
pub(crate) fn compute_from_base_sources<T: CohortMetricsBase>(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&T],
exit: &Exit,
) -> Result<()> {
self.supply.compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.supply()).collect::<Vec<_>>(),
exit,
)?;
self.outputs.compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.outputs()).collect::<Vec<_>>(),
exit,
)?;
self.activity.compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.activity_core()).collect::<Vec<_>>(),
exit,
)?;
self.realized.compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.realized_core()).collect::<Vec<_>>(),
exit,
)?;
self.unrealized.compute_from_stateful(
starting_indexes,
starting_lengths,
&others
.iter()
.map(|v| v.unrealized_core())
@@ -101,52 +102,52 @@ impl CoreCohortMetrics {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply.compute(prices, starting_indexes.height, exit)?;
self.supply.compute(prices, starting_lengths.height, exit)?;
self.outputs.compute_rest(starting_indexes.height, exit)?;
self.outputs.compute_rest(starting_lengths.height, exit)?;
self.activity
.compute_rest_part1(prices, starting_indexes, exit)?;
.compute_rest_part1(prices, starting_lengths, exit)?;
self.realized.compute_rest_part1(starting_indexes, exit)?;
self.realized.compute_rest_part1(starting_lengths, exit)?;
self.unrealized.compute_rest(starting_indexes, exit)?;
self.unrealized.compute_rest(starting_lengths, exit)?;
Ok(())
}
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
prices,
starting_indexes,
starting_lengths,
&self.supply.total.btc.height,
&self.activity.transfer_volume.sum._24h.cents.height,
exit,
)?;
self.unrealized.compute(
starting_indexes.height,
starting_lengths.height,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
.compute_dominance(starting_lengths.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
.compute_part2(starting_lengths.height, all_utxo_count, exit)?;
Ok(())
}
@@ -1,7 +1,8 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Indexes, Sats, StoredU64, Version};
use brk_types::{Dollars, Height, Sats, StoredU64, Version};
use vecdb::AnyStoredVec;
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
@@ -11,7 +12,7 @@ use crate::{
ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase, RealizedFull,
RelativeWithExtended, SupplyCore, UnrealizedFull,
},
prices,
price,
};
/// Cohort metrics with extended realized + extended cost basis (no adjusted).
@@ -89,8 +90,8 @@ impl ExtendedCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
@@ -99,7 +100,7 @@ impl ExtendedCohortMetrics {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
starting_lengths,
&self.supply.total.btc.height,
height_to_market_cap,
&self.activity.transfer_volume,
@@ -107,14 +108,14 @@ impl ExtendedCohortMetrics {
)?;
self.unrealized.compute(
starting_indexes.height,
starting_lengths.height,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.cost_basis.compute_prices(
starting_indexes,
starting_lengths,
&prices.spot.cents.height,
&self.unrealized.invested_capital.in_profit.cents.height,
&self.unrealized.invested_capital.in_loss.cents.height,
@@ -126,13 +127,13 @@ impl ExtendedCohortMetrics {
)?;
self.unrealized
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
.compute_sentiment(starting_lengths, &prices.spot.cents.height, exit)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
.compute_dominance(starting_lengths.height, all_supply_sats, exit)?;
self.relative.compute(
starting_indexes.height,
starting_lengths.height,
&self.supply,
&self.unrealized,
&self.realized,
@@ -142,7 +143,7 @@ impl ExtendedCohortMetrics {
)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
.compute_part2(starting_lengths.height, all_utxo_count, exit)?;
Ok(())
}
@@ -1,6 +1,7 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Indexes, Sats, StoredU64, Version};
use brk_types::{Cents, Dollars, Height, Sats, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
@@ -9,7 +10,7 @@ use crate::{
distribution::metrics::{
ActivityFull, AdjustedSopr, CohortMetricsBase, ImportConfig, RealizedFull, UnrealizedFull,
},
prices,
price,
};
use super::ExtendedCohortMetrics;
@@ -61,8 +62,8 @@ impl ExtendedAdjustedCohortMetrics {
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
under_1h_value_created: &impl ReadableVec<Height, Cents>,
under_1h_value_destroyed: &impl ReadableVec<Height, Cents>,
@@ -73,7 +74,7 @@ impl ExtendedAdjustedCohortMetrics {
self.inner.compute_rest_part2(
blocks,
prices,
starting_indexes,
starting_lengths,
height_to_market_cap,
all_supply_sats,
all_utxo_count,
@@ -81,7 +82,7 @@ impl ExtendedAdjustedCohortMetrics {
)?;
self.asopr.compute_rest_part2(
starting_indexes,
starting_lengths,
&self.inner.activity.transfer_volume.block.cents,
&self.inner.realized.core.sopr.value_destroyed.block,
under_1h_value_created,
@@ -1,14 +1,15 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, StoredU64};
use brk_types::{Height, Sats, StoredU64};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::metrics::{
ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyBase, UnrealizedMinimal,
},
prices,
price,
};
/// MinimalCohortMetrics: supply, outputs, realized cap/price/mvrv/profit/loss + value_created/destroyed.
@@ -58,17 +59,17 @@ impl MinimalCohortMetrics {
/// Aggregate Minimal-tier metrics from other MinimalCohortMetrics sources.
pub(crate) fn compute_from_sources(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&MinimalCohortMetrics],
exit: &Exit,
) -> Result<()> {
self.supply.compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.supply.as_ref()).collect::<Vec<_>>(),
exit,
)?;
self.outputs.compute_from_stateful(
starting_indexes,
starting_lengths,
&others
.iter()
.map(|v| v.outputs.as_ref())
@@ -76,7 +77,7 @@ impl MinimalCohortMetrics {
exit,
)?;
self.activity.compute_from_stateful(
starting_indexes,
starting_lengths,
&others
.iter()
.map(|v| v.activity.as_ref())
@@ -84,7 +85,7 @@ impl MinimalCohortMetrics {
exit,
)?;
self.realized.compute_from_stateful(
starting_indexes,
starting_lengths,
&others
.iter()
.map(|v| v.realized.as_ref())
@@ -96,45 +97,45 @@ impl MinimalCohortMetrics {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply.compute(prices, starting_indexes.height, exit)?;
self.outputs.compute_rest(starting_indexes.height, exit)?;
self.supply.compute(prices, starting_lengths.height, exit)?;
self.outputs.compute_rest(starting_lengths.height, exit)?;
self.activity
.compute_rest_part1(prices, starting_indexes, exit)?;
self.realized.compute_rest_part1(starting_indexes, exit)?;
.compute_rest_part1(prices, starting_lengths, exit)?;
self.realized.compute_rest_part1(starting_lengths, exit)?;
Ok(())
}
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
prices,
starting_indexes,
starting_lengths,
&self.supply.total.btc.height,
exit,
)?;
self.unrealized.compute(
starting_indexes.height,
starting_lengths.height,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
.compute_dominance(starting_lengths.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
.compute_part2(starting_lengths.height, all_utxo_count, exit)?;
Ok(())
}
@@ -1,14 +1,15 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, StoredU64};
use brk_types::{Height, Sats, StoredU64};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::metrics::{
ActivityMinimal, ImportConfig, OutputsBase, RealizedMinimal, SupplyCore, UnrealizedBasic,
},
prices,
price,
};
/// TypeCohortMetrics: supply(core), outputs(base), realized(minimal), unrealized(basic).
@@ -58,45 +59,45 @@ impl TypeCohortMetrics {
pub(crate) fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply.compute(prices, starting_indexes.height, exit)?;
self.outputs.compute_rest(starting_indexes.height, exit)?;
self.supply.compute(prices, starting_lengths.height, exit)?;
self.outputs.compute_rest(starting_lengths.height, exit)?;
self.activity
.compute_rest_part1(prices, starting_indexes, exit)?;
self.realized.compute_rest_part1(starting_indexes, exit)?;
.compute_rest_part1(prices, starting_lengths, exit)?;
self.realized.compute_rest_part1(starting_lengths, exit)?;
Ok(())
}
pub(crate) fn compute_rest_part2(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
prices,
starting_indexes,
starting_lengths,
&self.supply.total.btc.height,
exit,
)?;
self.unrealized.compute(
starting_indexes.height,
starting_lengths.height,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
.compute_dominance(starting_lengths.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
.compute_part2(starting_lengths.height, all_utxo_count, exit)?;
Ok(())
}
@@ -1,7 +1,8 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::CentsSquaredSats;
use brk_types::{BasisPoints16, Cents, Height, Indexes, Sats, Version};
use brk_types::{BasisPoints16, Cents, Height, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::internal::{PERCENTILES_LEN, PerBlock, PercentPerBlock, PercentilesVecs, Price};
@@ -147,7 +148,7 @@ impl CostBasis {
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_prices(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
spot: &impl ReadableVec<Height, Cents>,
invested_cap_in_profit: &impl ReadableVec<Height, Cents>,
invested_cap_in_loss: &impl ReadableVec<Height, Cents>,
@@ -158,7 +159,7 @@ impl CostBasis {
exit: &Exit,
) -> Result<()> {
self.in_profit.per_coin.cents.height.compute_transform3(
starting_indexes.height,
starting_lengths.height,
invested_cap_in_profit,
supply_in_profit_sats,
spot,
@@ -175,7 +176,7 @@ impl CostBasis {
exit,
)?;
self.in_loss.per_coin.cents.height.compute_transform3(
starting_indexes.height,
starting_lengths.height,
invested_cap_in_loss,
supply_in_loss_sats,
spot,
@@ -192,7 +193,7 @@ impl CostBasis {
exit,
)?;
self.in_profit.per_dollar.cents.height.compute_transform3(
starting_indexes.height,
starting_lengths.height,
capitalized_cap_in_profit_raw,
invested_cap_in_profit,
spot,
@@ -209,7 +210,7 @@ impl CostBasis {
exit,
)?;
self.in_loss.per_dollar.cents.height.compute_transform3(
starting_indexes.height,
starting_lengths.height,
capitalized_cap_in_loss_raw,
invested_cap_in_loss,
spot,
@@ -140,7 +140,8 @@ pub use unrealized::{
use brk_cohort::Filter;
use brk_error::Result;
use brk_types::{Cents, Indexes, Version};
use brk_indexer::Lengths;
use brk_types::{Cents, Version};
use vecdb::{AnyStoredVec, Exit, StorageMode};
use crate::{
@@ -148,7 +149,7 @@ use crate::{
CohortState, CoreRealizedState, CostBasisData, CostBasisOps, CostBasisRaw,
MinimalRealizedState, RealizedOps, RealizedState, WithCapital, WithoutCapital,
},
prices,
price,
};
pub trait CohortMetricsState {
@@ -269,24 +270,24 @@ pub trait CohortMetricsBase:
/// First phase of computed metrics (indexes from height).
fn compute_rest_part1(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.supply_mut()
.compute(prices, starting_indexes.height, exit)?;
.compute(prices, starting_lengths.height, exit)?;
self.outputs_mut()
.compute_rest(starting_indexes.height, exit)?;
.compute_rest(starting_lengths.height, exit)?;
self.activity_mut()
.compute_rest_part1(prices, starting_indexes, exit)?;
.compute_rest_part1(prices, starting_lengths, exit)?;
self.realized_mut()
.compute_rest_part1(starting_indexes, exit)?;
.compute_rest_part1(starting_lengths, exit)?;
let (supply, unrealized) = self.supply_and_unrealized_mut();
unrealized.compute_rest(
prices,
starting_indexes,
starting_lengths,
&supply.in_profit.sats.height,
&supply.in_loss.sats.height,
exit,
@@ -298,32 +299,32 @@ pub trait CohortMetricsBase:
/// Compute aggregate base metrics from source cohorts.
fn compute_base_from_others<T: CohortMetricsBase>(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&T],
exit: &Exit,
) -> Result<()> {
self.supply_mut().compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.supply()).collect::<Vec<_>>(),
exit,
)?;
self.outputs_mut().compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.outputs()).collect::<Vec<_>>(),
exit,
)?;
self.activity_mut().compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.activity_core()).collect::<Vec<_>>(),
exit,
)?;
self.realized_mut().compute_from_stateful(
starting_indexes,
starting_lengths,
&others.iter().map(|v| v.realized_core()).collect::<Vec<_>>(),
exit,
)?;
self.unrealized_core_mut().compute_from_stateful(
starting_indexes,
starting_lengths,
&others
.iter()
.map(|v| v.unrealized_core())
@@ -1,8 +1,7 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{
BasisPointsSigned32, Height, Indexes, StoredF32, StoredI64, StoredU32, StoredU64, Version,
};
use brk_types::{BasisPointsSigned32, Height, StoredF32, StoredI64, StoredU32, StoredU64, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{
@@ -83,19 +82,19 @@ impl OutputsBase {
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
starting_lengths: &Lengths,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.unspent_count.height.compute_sum_of_others(
starting_indexes.height,
starting_lengths.height,
&others
.iter()
.map(|v| &v.unspent_count.height)
.collect::<Vec<_>>(),
exit,
)?;
sum_others!(self, starting_indexes, others, exit; spent_count.block);
sum_others!(self, starting_lengths, others, exit; spent_count.block);
Ok(())
}
}
@@ -1,7 +1,8 @@
use brk_cohort::{Loss, Profit, ProfitabilityRange};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Dollars, Indexes, Sats, Version};
use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Dollars, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
@@ -9,7 +10,7 @@ use crate::{
internal::{
PerBlock, RatioPerBlock, ValuePerBlock, ValuePerBlockWithDeltas, WindowStartVec, Windows,
},
prices,
price,
};
#[derive(Traversable)]
@@ -114,12 +115,12 @@ impl ProfitabilityBucket {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
is_profit: bool,
exit: &Exit,
) -> Result<()> {
let max_from = starting_indexes.height;
let max_from = starting_lengths.height;
self.supply.all.compute(prices, max_from, exit)?;
self.supply.sth.compute(prices, max_from, exit)?;
@@ -175,13 +176,13 @@ impl ProfitabilityBucket {
pub(crate) fn compute_from_ranges(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
is_profit: bool,
sources: &[&ProfitabilityBucket],
exit: &Exit,
) -> Result<()> {
let max_from = starting_indexes.height;
let max_from = starting_lengths.height;
self.supply.all.sats.height.compute_sum_of_others(
max_from,
@@ -216,7 +217,7 @@ impl ProfitabilityBucket {
exit,
)?;
self.compute(prices, starting_indexes, is_profit, exit)
self.compute(prices, starting_lengths, is_profit, exit)
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
@@ -292,21 +293,21 @@ impl ProfitabilityMetrics {
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
for (is_profit, bucket) in self.range.iter_mut_with_is_profit() {
bucket.compute(prices, starting_indexes, is_profit, exit)?;
bucket.compute(prices, starting_lengths, is_profit, exit)?;
}
let range_arr = self.range.as_array();
for (threshold, sources) in self.profit.iter_mut_with_growing_prefix(&range_arr) {
threshold.compute_from_ranges(prices, starting_indexes, true, sources, exit)?;
threshold.compute_from_ranges(prices, starting_lengths, true, sources, exit)?;
}
for (threshold, sources) in self.loss.iter_mut_with_growing_suffix(&range_arr) {
threshold.compute_from_ranges(prices, starting_indexes, false, sources, exit)?;
threshold.compute_from_ranges(prices, starting_lengths, false, sources, exit)?;
}
Ok(())

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