mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snap
This commit is contained in:
@@ -8198,7 +8198,7 @@ pub struct BrkClient {
|
|||||||
|
|
||||||
impl BrkClient {
|
impl BrkClient {
|
||||||
/// Client version.
|
/// Client version.
|
||||||
pub const VERSION: &'static str = "v0.3.0-alpha.1";
|
pub const VERSION: &'static str = "v0.3.0-alpha.2";
|
||||||
|
|
||||||
/// Create a new client with the given base URL.
|
/// Create a new client with the given base URL.
|
||||||
pub fn new(base_url: impl Into<String>) -> Self {
|
pub fn new(base_url: impl Into<String>) -> Self {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ impl Vecs {
|
|||||||
|
|
||||||
self.hodl_bank.compute_cumulative_transformed_binary(
|
self.hodl_bank.compute_cumulative_transformed_binary(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.usd.height,
|
&prices.cached_spot_usd,
|
||||||
&self.vocdd_median_1y,
|
&self.vocdd_median_1y,
|
||||||
|price, median| StoredF64::from(f64::from(price) - f64::from(median)),
|
|price, median| StoredF64::from(f64::from(price) - f64::from(median)),
|
||||||
exit,
|
exit,
|
||||||
@@ -31,7 +31,7 @@ impl Vecs {
|
|||||||
|
|
||||||
self.value.height.compute_divide(
|
self.value.height.compute_divide(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.usd.height,
|
&prices.cached_spot_usd,
|
||||||
&self.hodl_bank,
|
&self.hodl_bank,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ impl Vecs {
|
|||||||
.compute(starting_indexes.height, exit, |vec| {
|
.compute(starting_indexes.height, exit, |vec| {
|
||||||
vec.compute_multiply(
|
vec.compute_multiply(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.usd.height,
|
&prices.cached_spot_usd,
|
||||||
&coinblocks_destroyed.block,
|
&coinblocks_destroyed.block,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
@@ -34,7 +34,7 @@ impl Vecs {
|
|||||||
self.created.compute(starting_indexes.height, exit, |vec| {
|
self.created.compute(starting_indexes.height, exit, |vec| {
|
||||||
vec.compute_multiply(
|
vec.compute_multiply(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.usd.height,
|
&prices.cached_spot_usd,
|
||||||
&activity.coinblocks_created.block,
|
&activity.coinblocks_created.block,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
@@ -44,7 +44,7 @@ impl Vecs {
|
|||||||
self.stored.compute(starting_indexes.height, exit, |vec| {
|
self.stored.compute(starting_indexes.height, exit, |vec| {
|
||||||
vec.compute_multiply(
|
vec.compute_multiply(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.usd.height,
|
&prices.cached_spot_usd,
|
||||||
&activity.coinblocks_stored.block,
|
&activity.coinblocks_stored.block,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
@@ -57,7 +57,7 @@ impl Vecs {
|
|||||||
self.vocdd.compute(starting_indexes.height, exit, |vec| {
|
self.vocdd.compute(starting_indexes.height, exit, |vec| {
|
||||||
vec.compute_transform3(
|
vec.compute_transform3(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.usd.height,
|
&prices.cached_spot_usd,
|
||||||
&coindays_destroyed.block,
|
&coindays_destroyed.block,
|
||||||
circulating_supply,
|
circulating_supply,
|
||||||
|(i, price, cdd, supply, _): (_, Dollars, StoredF64, Bitcoin, _)| {
|
|(i, price, cdd, supply, _): (_, Dollars, StoredF64, Bitcoin, _)| {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ impl AllCohortMetrics {
|
|||||||
|
|
||||||
self.unrealized.compute(
|
self.unrealized.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized.price.cents.height,
|
&self.realized.price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
@@ -139,7 +139,7 @@ impl AllCohortMetrics {
|
|||||||
|
|
||||||
self.cost_basis.compute_prices(
|
self.cost_basis.compute_prices(
|
||||||
starting_indexes,
|
starting_indexes,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.unrealized.invested_capital.in_profit.cents.height,
|
&self.unrealized.invested_capital.in_profit.cents.height,
|
||||||
&self.unrealized.invested_capital.in_loss.cents.height,
|
&self.unrealized.invested_capital.in_loss.cents.height,
|
||||||
&self.supply.in_profit.sats.height,
|
&self.supply.in_profit.sats.height,
|
||||||
@@ -150,7 +150,7 @@ impl AllCohortMetrics {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.unrealized
|
self.unrealized
|
||||||
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
|
.compute_sentiment(starting_indexes, &prices.cached_spot_cents, exit)?;
|
||||||
|
|
||||||
self.relative.compute(
|
self.relative.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ impl BasicCohortMetrics {
|
|||||||
|
|
||||||
self.unrealized.compute(
|
self.unrealized.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized.price.cents.height,
|
&self.realized.price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ impl CoreCohortMetrics {
|
|||||||
|
|
||||||
self.unrealized.compute(
|
self.unrealized.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized.price.cents.height,
|
&self.realized.price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -108,14 +108,14 @@ impl ExtendedCohortMetrics {
|
|||||||
|
|
||||||
self.unrealized.compute(
|
self.unrealized.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized.price.cents.height,
|
&self.realized.price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.cost_basis.compute_prices(
|
self.cost_basis.compute_prices(
|
||||||
starting_indexes,
|
starting_indexes,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.unrealized.invested_capital.in_profit.cents.height,
|
&self.unrealized.invested_capital.in_profit.cents.height,
|
||||||
&self.unrealized.invested_capital.in_loss.cents.height,
|
&self.unrealized.invested_capital.in_loss.cents.height,
|
||||||
&self.supply.in_profit.sats.height,
|
&self.supply.in_profit.sats.height,
|
||||||
@@ -126,7 +126,7 @@ impl ExtendedCohortMetrics {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.unrealized
|
self.unrealized
|
||||||
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
|
.compute_sentiment(starting_indexes, &prices.cached_spot_cents, exit)?;
|
||||||
|
|
||||||
self.relative.compute(
|
self.relative.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ impl MinimalCohortMetrics {
|
|||||||
|
|
||||||
self.unrealized.compute(
|
self.unrealized.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized.price.cents.height,
|
&self.realized.price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ impl TypeCohortMetrics {
|
|||||||
|
|
||||||
self.unrealized.compute(
|
self.unrealized.compute(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized.price.cents.height,
|
&self.realized.price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ impl ProfitabilityBucket {
|
|||||||
|
|
||||||
self.unrealized_pnl.all.height.compute_transform3(
|
self.unrealized_pnl.all.height.compute_transform3(
|
||||||
max_from,
|
max_from,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized_cap.all.height,
|
&self.realized_cap.all.height,
|
||||||
&self.supply.all.sats.height,
|
&self.supply.all.sats.height,
|
||||||
|(i, spot, cap, supply, ..)| {
|
|(i, spot, cap, supply, ..)| {
|
||||||
@@ -139,7 +139,7 @@ impl ProfitabilityBucket {
|
|||||||
)?;
|
)?;
|
||||||
self.unrealized_pnl.sth.height.compute_transform3(
|
self.unrealized_pnl.sth.height.compute_transform3(
|
||||||
max_from,
|
max_from,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized_cap.sth.height,
|
&self.realized_cap.sth.height,
|
||||||
&self.supply.sth.sats.height,
|
&self.supply.sth.sats.height,
|
||||||
|(i, spot, cap, supply, ..)| {
|
|(i, spot, cap, supply, ..)| {
|
||||||
@@ -153,7 +153,7 @@ impl ProfitabilityBucket {
|
|||||||
|
|
||||||
self.nupl.bps.height.compute_transform3(
|
self.nupl.bps.height.compute_transform3(
|
||||||
max_from,
|
max_from,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.realized_cap.all.height,
|
&self.realized_cap.all.height,
|
||||||
&self.supply.all.sats.height,
|
&self.supply.all.sats.height,
|
||||||
|(i, spot, cap_dollars, supply_sats, ..)| {
|
|(i, spot, cap_dollars, supply_sats, ..)| {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ impl UnrealizedFull {
|
|||||||
.compute_transform3(
|
.compute_transform3(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
supply_in_profit_sats,
|
supply_in_profit_sats,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.inner.basic.profit.cents.height,
|
&self.inner.basic.profit.cents.height,
|
||||||
|(h, supply_sats, spot, profit, ..): (_, Sats, Cents, Cents, _)| {
|
|(h, supply_sats, spot, profit, ..): (_, Sats, Cents, Cents, _)| {
|
||||||
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
|
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
|
||||||
@@ -142,7 +142,7 @@ impl UnrealizedFull {
|
|||||||
.compute_transform3(
|
.compute_transform3(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
supply_in_loss_sats,
|
supply_in_loss_sats,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.inner.basic.loss.cents.height,
|
&self.inner.basic.loss.cents.height,
|
||||||
|(h, supply_sats, spot, loss, ..): (_, Sats, Cents, Cents, _)| {
|
|(h, supply_sats, spot, loss, ..): (_, Sats, Cents, Cents, _)| {
|
||||||
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
|
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use brk_error::Result;
|
use brk_error::Result;
|
||||||
use brk_traversable::Traversable;
|
use brk_traversable::Traversable;
|
||||||
use brk_types::{Cents, Height, Indexes, StoredI8, Version};
|
use brk_types::{Cents, Height, Indexes, StoredI8, Version};
|
||||||
use vecdb::{AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, WritableVec};
|
use vecdb::{AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cointime, distribution, indexes,
|
cointime, distribution, indexes,
|
||||||
@@ -123,7 +123,7 @@ impl RealizedEnvelope {
|
|||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let spot = &prices.spot.cents.height;
|
let spot = &prices.cached_spot_cents;
|
||||||
|
|
||||||
// Zone: spot vs own envelope bands (-4 to +4)
|
// Zone: spot vs own envelope bands (-4 to +4)
|
||||||
self.compute_index(spot, starting_indexes, exit)?;
|
self.compute_index(spot, starting_indexes, exit)?;
|
||||||
@@ -136,7 +136,7 @@ impl RealizedEnvelope {
|
|||||||
|
|
||||||
fn compute_index(
|
fn compute_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
spot: &EagerVec<PcoVec<Height, Cents>>,
|
spot: &impl ReadableVec<Height, Cents>,
|
||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -214,7 +214,7 @@ impl RealizedEnvelope {
|
|||||||
fn compute_score(
|
fn compute_score(
|
||||||
&mut self,
|
&mut self,
|
||||||
models: &[&RatioPerBlockPercentiles; 10],
|
models: &[&RatioPerBlockPercentiles; 10],
|
||||||
spot: &EagerVec<PcoVec<Height, Cents>>,
|
spot: &impl ReadableVec<Height, Cents>,
|
||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ impl AmountPerBlock {
|
|||||||
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
||||||
max_from,
|
max_from,
|
||||||
&self.sats.height,
|
&self.sats.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ impl AmountBlock {
|
|||||||
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
||||||
max_from,
|
max_from,
|
||||||
&self.sats,
|
&self.sats,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
|
|||||||
let count_indexes_batch: Vec<brk_types::StoredU64> =
|
let count_indexes_batch: Vec<brk_types::StoredU64> =
|
||||||
count_indexes.collect_range_at(start, fi_len);
|
count_indexes.collect_range_at(start, fi_len);
|
||||||
|
|
||||||
|
let zero = T::from(0_usize);
|
||||||
let mut values: Vec<T> = Vec::new();
|
let mut values: Vec<T> = Vec::new();
|
||||||
|
|
||||||
first_indexes_batch
|
first_indexes_batch
|
||||||
@@ -114,8 +115,11 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
|
|||||||
&mut values,
|
&mut values,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if skip_count > 0 {
|
||||||
|
values.retain(|v| *v > zero);
|
||||||
|
}
|
||||||
|
|
||||||
if values.is_empty() {
|
if values.is_empty() {
|
||||||
let zero = T::from(0_usize);
|
|
||||||
for vec in [
|
for vec in [
|
||||||
&mut *min,
|
&mut *min,
|
||||||
&mut *max,
|
&mut *max,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ impl PriceWithRatioPerBlock {
|
|||||||
F: FnMut(&mut EagerVec<PcoVec<Height, Cents>>) -> Result<()>,
|
F: FnMut(&mut EagerVec<PcoVec<Height, Cents>>) -> Result<()>,
|
||||||
{
|
{
|
||||||
compute_price(&mut self.cents.height)?;
|
compute_price(&mut self.cents.height)?;
|
||||||
self.compute_ratio(starting_indexes, &prices.spot.cents.height, exit)
|
self.compute_ratio(starting_indexes, &prices.cached_spot_cents, exit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ impl PriceWithRatioExtendedPerBlock {
|
|||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let close_price = &prices.spot.cents.height;
|
let close_price = &prices.cached_spot_cents;
|
||||||
self.base
|
self.base
|
||||||
.compute_ratio(starting_indexes, close_price, exit)?;
|
.compute_ratio(starting_indexes, close_price, exit)?;
|
||||||
self.percentiles.compute(
|
self.percentiles.compute(
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ impl Vecs {
|
|||||||
{
|
{
|
||||||
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&average_price.cents.height,
|
&average_price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
@@ -163,7 +163,7 @@ impl Vecs {
|
|||||||
{
|
{
|
||||||
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&lookback_price.cents.height,
|
&lookback_price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
@@ -266,7 +266,7 @@ impl Vecs {
|
|||||||
{
|
{
|
||||||
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&average_price.cents.height,
|
&average_price.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ impl Vecs {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.high.cents.height.compute_all_time_high(
|
self.high.cents.height.compute_all_time_high(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ impl Vecs {
|
|||||||
self.days_since.height.compute_transform3(
|
self.days_since.height.compute_transform3(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&self.high.cents.height,
|
&self.high.cents.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&indexes.timestamp.monotonic,
|
&indexes.timestamp.monotonic,
|
||||||
|(i, ath, price, ts, slf)| {
|
|(i, ath, price, ts, slf)| {
|
||||||
if ath_ts.is_none() {
|
if ath_ts.is_none() {
|
||||||
@@ -68,7 +68,7 @@ impl Vecs {
|
|||||||
|
|
||||||
self.drawdown.compute_drawdown(
|
self.drawdown.compute_drawdown(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.cents.height,
|
&prices.cached_spot_cents,
|
||||||
&self.high.cents.height,
|
&self.high.cents.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ impl Vecs {
|
|||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let price = &prices.spot.cents.height;
|
let price = &prices.cached_spot_cents;
|
||||||
|
|
||||||
for (price_past, days) in self.price_past.iter_mut_with_days() {
|
for (price_past, days) in self.price_past.iter_mut_with_days() {
|
||||||
let window_starts = blocks.lookback.start_vec(days as usize);
|
let window_starts = blocks.lookback.start_vec(days as usize);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ impl Vecs {
|
|||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let close = &prices.spot.cents.height;
|
let close = &prices.cached_spot_cents;
|
||||||
|
|
||||||
for (sma, period) in [
|
for (sma, period) in [
|
||||||
(&mut self.sma._1w, 7),
|
(&mut self.sma._1w, 7),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ impl Vecs {
|
|||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let price = &prices.spot.cents.height;
|
let price = &prices.cached_spot_cents;
|
||||||
|
|
||||||
for (min_vec, max_vec, starts) in [
|
for (min_vec, max_vec, starts) in [
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ impl Vecs {
|
|||||||
{
|
{
|
||||||
returns.compute_binary::<Dollars, Dollars, RatioDiffDollarsBps32>(
|
returns.compute_binary::<Dollars, Dollars, RatioDiffDollarsBps32>(
|
||||||
starting_indexes.height,
|
starting_indexes.height,
|
||||||
&prices.spot.usd.height,
|
&prices.cached_spot_usd,
|
||||||
&lookback_price.usd.height,
|
&lookback_price.usd.height,
|
||||||
exit,
|
exit,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub(super) fn compute(
|
|||||||
starting_indexes: &Indexes,
|
starting_indexes: &Indexes,
|
||||||
exit: &Exit,
|
exit: &Exit,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let close = &prices.spot.usd.height;
|
let close = &prices.cached_spot_usd;
|
||||||
let ws_fast = blocks.lookback.start_vec(fast_days);
|
let ws_fast = blocks.lookback.start_vec(fast_days);
|
||||||
let ws_slow = blocks.lookback.start_vec(slow_days);
|
let ws_slow = blocks.lookback.start_vec(slow_days);
|
||||||
let ws_signal = blocks.lookback.start_vec(signal_days);
|
let ws_signal = blocks.lookback.start_vec(signal_days);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use std::path::Path;
|
|||||||
|
|
||||||
use brk_traversable::Traversable;
|
use brk_traversable::Traversable;
|
||||||
use brk_types::Version;
|
use brk_types::Version;
|
||||||
use vecdb::{Database, ReadableCloneableVec, Rw, StorageMode};
|
use brk_types::{Cents, Dollars, Height};
|
||||||
|
use vecdb::{CachedVec, Database, LazyVecFrom1, ReadableCloneableVec, Rw, StorageMode};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
indexes,
|
indexes,
|
||||||
@@ -27,6 +28,11 @@ pub struct Vecs<M: StorageMode = Rw> {
|
|||||||
#[traversable(skip)]
|
#[traversable(skip)]
|
||||||
pub db: Database,
|
pub db: Database,
|
||||||
|
|
||||||
|
#[traversable(skip)]
|
||||||
|
pub cached_spot_cents: CachedVec<Height, Cents>,
|
||||||
|
#[traversable(skip)]
|
||||||
|
pub cached_spot_usd: LazyVecFrom1<Height, Dollars, Height, Cents>,
|
||||||
|
|
||||||
pub split: SplitByUnit<M>,
|
pub split: SplitByUnit<M>,
|
||||||
pub ohlc: OhlcByUnit<M>,
|
pub ohlc: OhlcByUnit<M>,
|
||||||
pub spot: PriceByUnit<M>,
|
pub spot: PriceByUnit<M>,
|
||||||
@@ -169,6 +175,13 @@ impl Vecs {
|
|||||||
sats: ohlc_sats,
|
sats: ohlc_sats,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let cached_spot_cents = CachedVec::new(&price_cents.height);
|
||||||
|
let cached_spot_usd = LazyVecFrom1::transformed::<CentsUnsignedToDollars>(
|
||||||
|
"price",
|
||||||
|
version,
|
||||||
|
cached_spot_cents.read_only_boxed_clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let spot = PriceByUnit {
|
let spot = PriceByUnit {
|
||||||
usd: price_usd,
|
usd: price_usd,
|
||||||
cents: price_cents,
|
cents: price_cents,
|
||||||
@@ -177,6 +190,8 @@ impl Vecs {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
|
cached_spot_cents,
|
||||||
|
cached_spot_usd,
|
||||||
split,
|
split,
|
||||||
ohlc,
|
ohlc,
|
||||||
spot,
|
spot,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use vecdb::{Database, EagerVec, ImportableVec};
|
|||||||
use super::Vecs;
|
use super::Vecs;
|
||||||
use crate::{indexes, internal::PerTxDistribution};
|
use crate::{indexes, internal::PerTxDistribution};
|
||||||
|
|
||||||
/// Bump this when fee/feerate aggregation logic changes (e.g., skip coinbase).
|
/// Bump this when fee/feerate aggregation logic changes (e.g., skip coinbase, skip zero-fee).
|
||||||
const VERSION: Version = Version::new(2);
|
const VERSION: Version = Version::new(3);
|
||||||
|
|
||||||
impl Vecs {
|
impl Vecs {
|
||||||
pub(crate) fn forced_import(
|
pub(crate) fn forced_import(
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ pub fn init(path: Option<&Path>) -> io::Result<()> {
|
|||||||
let level = std::env::var("LOG").unwrap_or_else(|_| DEFAULT_LEVEL.to_string());
|
let level = std::env::var("LOG").unwrap_or_else(|_| DEFAULT_LEVEL.to_string());
|
||||||
|
|
||||||
let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| {
|
let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| {
|
||||||
// fjall=off,lsm_tree=off
|
|
||||||
format!(
|
format!(
|
||||||
"{level},bitcoin=off,bitcoincore_rpc=off,corepc=off,tracing=off,aide=off,tower_http=off"
|
"{level},bitcoin=off,bitcoincore_rpc=off,corepc=off,tracing=off,aide=off,fjall=off,lsm_tree=off,tower_http=off"
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn block_by_height(&self, height: Height) -> Result<BlockInfo> {
|
pub fn block_by_height(&self, height: Height) -> Result<BlockInfo> {
|
||||||
let max_height = self.max_height();
|
let max_height = self.indexed_height();
|
||||||
if height > max_height {
|
if height > max_height {
|
||||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn block_by_height_v1(&self, height: Height) -> Result<BlockInfoV1> {
|
pub fn block_by_height_v1(&self, height: Height) -> Result<BlockInfoV1> {
|
||||||
let max_height = self.max_height();
|
let max_height = self.height();
|
||||||
if height > max_height {
|
if height > max_height {
|
||||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn block_hash_by_height(&self, height: Height) -> Result<BlockHash> {
|
pub fn block_hash_by_height(&self, height: Height) -> Result<BlockHash> {
|
||||||
let max_height = self.max_height();
|
let max_height = self.indexed_height();
|
||||||
if height > max_height {
|
if height > max_height {
|
||||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||||
}
|
}
|
||||||
@@ -219,6 +219,7 @@ impl Query {
|
|||||||
.block
|
.block
|
||||||
.sats
|
.sats
|
||||||
.collect_range_at(begin, end);
|
.collect_range_at(begin, end);
|
||||||
|
let prices = computer.prices.cached_spot_usd.collect_range_at(begin, end);
|
||||||
let output_volumes = computer
|
let output_volumes = computer
|
||||||
.mining
|
.mining
|
||||||
.rewards
|
.rewards
|
||||||
@@ -381,6 +382,7 @@ impl Query {
|
|||||||
utxo_set_size: *utxo_set_sizes[i],
|
utxo_set_size: *utxo_set_sizes[i],
|
||||||
total_input_amt,
|
total_input_amt,
|
||||||
virtual_size: vsize as f64,
|
virtual_size: vsize as f64,
|
||||||
|
price: prices[i],
|
||||||
};
|
};
|
||||||
|
|
||||||
blocks.push(BlockInfoV1 { info, extras });
|
blocks.push(BlockInfoV1 { info, extras });
|
||||||
@@ -415,10 +417,6 @@ impl Query {
|
|||||||
.map_err(|_| Error::Internal("Failed to decode block header"))
|
.map_err(|_| Error::Internal("Failed to decode block header"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max_height(&self) -> Height {
|
|
||||||
Height::from(self.indexer().vecs.blocks.blockhash.len().saturating_sub(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_block_range(&self, start_height: Option<Height>, count: u32) -> (usize, usize) {
|
fn resolve_block_range(&self, start_height: Option<Height>, count: u32) -> (usize, usize) {
|
||||||
let max_height = self.height();
|
let max_height = self.height();
|
||||||
let start = start_height.unwrap_or(max_height).min(max_height);
|
let start = start_height.unwrap_or(max_height).min(max_height);
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ impl BlockWindow {
|
|||||||
.collect_range_at(self.start, self.end);
|
.collect_range_at(self.start, self.end);
|
||||||
let all_prices: Vec<Cents> = computer
|
let all_prices: Vec<Cents> = computer
|
||||||
.prices
|
.prices
|
||||||
.spot
|
.cached_spot_cents
|
||||||
.cents
|
|
||||||
.height
|
|
||||||
.collect_range_at(self.start, self.end);
|
.collect_range_at(self.start, self.end);
|
||||||
let read_start = self.start.saturating_sub(1).max(0);
|
let read_start = self.start.saturating_sub(1).max(0);
|
||||||
let all_cum = cumulative.collect_range_at(read_start, self.end);
|
let all_cum = cumulative.collect_range_at(read_start, self.end);
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ impl BlockRoutes for ApiRouter<AppState> {
|
|||||||
"/api/blocks/tip/height",
|
"/api/blocks/tip/height",
|
||||||
get_with(
|
get_with(
|
||||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||||
state.cached_text(&headers, CacheStrategy::Tip, &uri, |q| Ok(q.height().to_string())).await
|
state.cached_text(&headers, CacheStrategy::Tip, &uri, |q| Ok(q.indexed_height().to_string())).await
|
||||||
},
|
},
|
||||||
|op| {
|
|op| {
|
||||||
op.id("get_block_tip_height")
|
op.id("get_block_tip_height")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{BlockPool, FeeRate, Sats, Weight};
|
use crate::{BlockPool, Dollars, FeeRate, Sats, Weight};
|
||||||
|
|
||||||
/// Extended block data matching mempool.space /api/v1/blocks extras
|
/// Extended block data matching mempool.space /api/v1/blocks extras
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
@@ -106,4 +106,7 @@ pub struct BlockExtras {
|
|||||||
/// Virtual size in vbytes
|
/// Virtual size in vbytes
|
||||||
#[serde(rename = "virtualSize")]
|
#[serde(rename = "virtualSize")]
|
||||||
pub virtual_size: f64,
|
pub virtual_size: f64,
|
||||||
|
|
||||||
|
/// USD price at block height
|
||||||
|
pub price: Dollars,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@
|
|||||||
* @property {number} utxoSetSize - Total UTXO set size at this height
|
* @property {number} utxoSetSize - Total UTXO set size at this height
|
||||||
* @property {Sats} totalInputAmt - Total input amount in satoshis
|
* @property {Sats} totalInputAmt - Total input amount in satoshis
|
||||||
* @property {number} virtualSize - Virtual size in vbytes
|
* @property {number} virtualSize - Virtual size in vbytes
|
||||||
|
* @property {Dollars} price - USD price at block height
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* A single block fees data point.
|
* A single block fees data point.
|
||||||
@@ -6563,7 +6564,7 @@ function createTransferPattern(client, acc) {
|
|||||||
* @extends BrkClientBase
|
* @extends BrkClientBase
|
||||||
*/
|
*/
|
||||||
class BrkClient extends BrkClientBase {
|
class BrkClient extends BrkClientBase {
|
||||||
VERSION = "v0.3.0-alpha.1";
|
VERSION = "v0.3.0-alpha.2";
|
||||||
|
|
||||||
INDEXES = /** @type {const} */ ([
|
INDEXES = /** @type {const} */ ([
|
||||||
"minute10",
|
"minute10",
|
||||||
|
|||||||
@@ -40,5 +40,5 @@
|
|||||||
"url": "git+https://github.com/bitcoinresearchkit/brk.git"
|
"url": "git+https://github.com/bitcoinresearchkit/brk.git"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.3.0-alpha.1"
|
"version": "0.3.0-alpha.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ BasisPointsSigned32 = int
|
|||||||
# Bitcoin amount as floating point (1 BTC = 100,000,000 satoshis)
|
# Bitcoin amount as floating point (1 BTC = 100,000,000 satoshis)
|
||||||
Bitcoin = float
|
Bitcoin = float
|
||||||
PoolSlug = Literal["unknown", "blockfills", "ultimuspool", "terrapool", "luxor", "onethash", "btccom", "bitfarms", "huobipool", "wayicn", "canoepool", "btctop", "bitcoincom", "pool175btc", "gbminers", "axbt", "asicminer", "bitminter", "bitcoinrussia", "btcserv", "simplecoinus", "btcguild", "eligius", "ozcoin", "eclipsemc", "maxbtc", "triplemining", "coinlab", "pool50btc", "ghashio", "stminingcorp", "bitparking", "mmpool", "polmine", "kncminer", "bitalo", "f2pool", "hhtt", "megabigpower", "mtred", "nmcbit", "yourbtcnet", "givemecoins", "braiinspool", "antpool", "multicoinco", "bcpoolio", "cointerra", "kanopool", "solock", "ckpool", "nicehash", "bitclub", "bitcoinaffiliatenetwork", "btcc", "bwpool", "exxbw", "bitsolo", "bitfury", "twentyoneinc", "digitalbtc", "eightbaochi", "mybtccoinpool", "tbdice", "hashpool", "nexious", "bravomining", "hotpool", "okexpool", "bcmonster", "onehash", "bixin", "tatmaspool", "viabtc", "connectbtc", "batpool", "waterhole", "dcexploration", "dcex", "btpool", "fiftyeightcoin", "bitcoinindia", "shawnp0wers", "phashio", "rigpool", "haozhuzhu", "sevenpool", "miningkings", "hashbx", "dpool", "rawpool", "haominer", "helix", "bitcoinukraine", "poolin", "secretsuperstar", "tigerpoolnet", "sigmapoolcom", "okpooltop", "hummerpool", "tangpool", "bytepool", "spiderpool", "novablock", "miningcity", "binancepool", "minerium", "lubiancom", "okkong", "aaopool", "emcdpool", "foundryusa", "sbicrypto", "arkpool", "purebtccom", "marapool", "kucoinpool", "entrustcharitypool", "okminer", "titan", "pegapool", "btcnuggets", "cloudhashing", "digitalxmintsy", "telco214", "btcpoolparty", "multipool", "transactioncoinmining", "btcdig", "trickysbtcpool", "btcmp", "eobot", "unomp", "patels", "gogreenlight", "bitcoinindiapool", "ekanembtc", "canoe", "tiger", "onem1x", "zulupool", "secpool", "ocean", "whitepool", "wiz", "wk057", "futurebitapollosolo", "carbonnegative", "portlandhodl", "phoenix", "neopool", "maxipool", "bitfufupool", "gdpool", "miningdutch", "publicpool", "miningsquared", "innopolistech", "btclab", "parasite", "redrockpool", "est3lar", "braiinssolo", "solopool"]
|
PoolSlug = Literal["unknown", "blockfills", "ultimuspool", "terrapool", "luxor", "onethash", "btccom", "bitfarms", "huobipool", "wayicn", "canoepool", "btctop", "bitcoincom", "pool175btc", "gbminers", "axbt", "asicminer", "bitminter", "bitcoinrussia", "btcserv", "simplecoinus", "btcguild", "eligius", "ozcoin", "eclipsemc", "maxbtc", "triplemining", "coinlab", "pool50btc", "ghashio", "stminingcorp", "bitparking", "mmpool", "polmine", "kncminer", "bitalo", "f2pool", "hhtt", "megabigpower", "mtred", "nmcbit", "yourbtcnet", "givemecoins", "braiinspool", "antpool", "multicoinco", "bcpoolio", "cointerra", "kanopool", "solock", "ckpool", "nicehash", "bitclub", "bitcoinaffiliatenetwork", "btcc", "bwpool", "exxbw", "bitsolo", "bitfury", "twentyoneinc", "digitalbtc", "eightbaochi", "mybtccoinpool", "tbdice", "hashpool", "nexious", "bravomining", "hotpool", "okexpool", "bcmonster", "onehash", "bixin", "tatmaspool", "viabtc", "connectbtc", "batpool", "waterhole", "dcexploration", "dcex", "btpool", "fiftyeightcoin", "bitcoinindia", "shawnp0wers", "phashio", "rigpool", "haozhuzhu", "sevenpool", "miningkings", "hashbx", "dpool", "rawpool", "haominer", "helix", "bitcoinukraine", "poolin", "secretsuperstar", "tigerpoolnet", "sigmapoolcom", "okpooltop", "hummerpool", "tangpool", "bytepool", "spiderpool", "novablock", "miningcity", "binancepool", "minerium", "lubiancom", "okkong", "aaopool", "emcdpool", "foundryusa", "sbicrypto", "arkpool", "purebtccom", "marapool", "kucoinpool", "entrustcharitypool", "okminer", "titan", "pegapool", "btcnuggets", "cloudhashing", "digitalxmintsy", "telco214", "btcpoolparty", "multipool", "transactioncoinmining", "btcdig", "trickysbtcpool", "btcmp", "eobot", "unomp", "patels", "gogreenlight", "bitcoinindiapool", "ekanembtc", "canoe", "tiger", "onem1x", "zulupool", "secpool", "ocean", "whitepool", "wiz", "wk057", "futurebitapollosolo", "carbonnegative", "portlandhodl", "phoenix", "neopool", "maxipool", "bitfufupool", "gdpool", "miningdutch", "publicpool", "miningsquared", "innopolistech", "btclab", "parasite", "redrockpool", "est3lar", "braiinssolo", "solopool"]
|
||||||
|
# US Dollar amount as floating point
|
||||||
|
Dollars = float
|
||||||
# Fee rate in sats/vB
|
# Fee rate in sats/vB
|
||||||
FeeRate = float
|
FeeRate = float
|
||||||
# Transaction or block weight in weight units (WU)
|
# Transaction or block weight in weight units (WU)
|
||||||
Weight = int
|
Weight = int
|
||||||
# US Dollar amount as floating point
|
|
||||||
Dollars = float
|
|
||||||
# Block height
|
# Block height
|
||||||
Height = int
|
Height = int
|
||||||
# UNIX timestamp in seconds
|
# UNIX timestamp in seconds
|
||||||
@@ -354,6 +354,7 @@ class BlockExtras(TypedDict):
|
|||||||
utxoSetSize: Total UTXO set size at this height
|
utxoSetSize: Total UTXO set size at this height
|
||||||
totalInputAmt: Total input amount in satoshis
|
totalInputAmt: Total input amount in satoshis
|
||||||
virtualSize: Virtual size in vbytes
|
virtualSize: Virtual size in vbytes
|
||||||
|
price: USD price at block height
|
||||||
"""
|
"""
|
||||||
totalFees: Sats
|
totalFees: Sats
|
||||||
medianFee: FeeRate
|
medianFee: FeeRate
|
||||||
@@ -381,6 +382,7 @@ class BlockExtras(TypedDict):
|
|||||||
utxoSetSize: int
|
utxoSetSize: int
|
||||||
totalInputAmt: Sats
|
totalInputAmt: Sats
|
||||||
virtualSize: float
|
virtualSize: float
|
||||||
|
price: Dollars
|
||||||
|
|
||||||
class BlockFeesEntry(TypedDict):
|
class BlockFeesEntry(TypedDict):
|
||||||
"""
|
"""
|
||||||
@@ -6001,7 +6003,7 @@ class SeriesTree:
|
|||||||
class BrkClient(BrkClientBase):
|
class BrkClient(BrkClientBase):
|
||||||
"""Main BRK client with series tree and API methods."""
|
"""Main BRK client with series tree and API methods."""
|
||||||
|
|
||||||
VERSION = "v0.3.0-alpha.1"
|
VERSION = "v0.3.0-alpha.2"
|
||||||
|
|
||||||
INDEXES = [
|
INDEXES = [
|
||||||
"minute10",
|
"minute10",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "brk-client"
|
name = "brk-client"
|
||||||
version = "0.3.0-alpha.1"
|
version = "0.3.0-alpha.2"
|
||||||
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
|
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ function renderDetails(block) {
|
|||||||
|
|
||||||
if (extras) {
|
if (extras) {
|
||||||
rows.push(
|
rows.push(
|
||||||
|
["Price", `$${extras.price.toLocaleString()}`],
|
||||||
["Pool", extras.pool.name],
|
["Pool", extras.pool.name],
|
||||||
["Pool ID", extras.pool.id.toString()],
|
["Pool ID", extras.pool.id.toString()],
|
||||||
["Pool Slug", extras.pool.slug],
|
["Pool Slug", extras.pool.slug],
|
||||||
|
|||||||
Reference in New Issue
Block a user