global: reused + mempool + favicon
@@ -1277,6 +1277,20 @@ impl<T: DeserializeOwned> AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90S
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern {
|
||||
pub all: BtcCentsSatsUsdPattern,
|
||||
pub p2a: BtcCentsSatsUsdPattern,
|
||||
pub p2pk33: BtcCentsSatsUsdPattern,
|
||||
pub p2pk65: BtcCentsSatsUsdPattern,
|
||||
pub p2pkh: BtcCentsSatsUsdPattern,
|
||||
pub p2sh: BtcCentsSatsUsdPattern,
|
||||
pub p2tr: BtcCentsSatsUsdPattern,
|
||||
pub p2wpkh: BtcCentsSatsUsdPattern,
|
||||
pub p2wsh: BtcCentsSatsUsdPattern,
|
||||
pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern {
|
||||
pub index: SeriesPattern1<StoredI8>,
|
||||
@@ -1339,6 +1353,36 @@ impl AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 {
|
||||
pub all: BpsPercentRatioPattern2,
|
||||
pub p2a: BpsPercentRatioPattern2,
|
||||
pub p2pk33: BpsPercentRatioPattern2,
|
||||
pub p2pk65: BpsPercentRatioPattern2,
|
||||
pub p2pkh: BpsPercentRatioPattern2,
|
||||
pub p2sh: BpsPercentRatioPattern2,
|
||||
pub p2tr: BpsPercentRatioPattern2,
|
||||
pub p2wpkh: BpsPercentRatioPattern2,
|
||||
pub p2wsh: BpsPercentRatioPattern2,
|
||||
}
|
||||
|
||||
impl AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
all: BpsPercentRatioPattern2::new(client.clone(), acc.clone()),
|
||||
p2a: BpsPercentRatioPattern2::new(client.clone(), _p("p2a", &acc)),
|
||||
p2pk33: BpsPercentRatioPattern2::new(client.clone(), _p("p2pk33", &acc)),
|
||||
p2pk65: BpsPercentRatioPattern2::new(client.clone(), _p("p2pk65", &acc)),
|
||||
p2pkh: BpsPercentRatioPattern2::new(client.clone(), _p("p2pkh", &acc)),
|
||||
p2sh: BpsPercentRatioPattern2::new(client.clone(), _p("p2sh", &acc)),
|
||||
p2tr: BpsPercentRatioPattern2::new(client.clone(), _p("p2tr", &acc)),
|
||||
p2wpkh: BpsPercentRatioPattern2::new(client.clone(), _p("p2wpkh", &acc)),
|
||||
p2wsh: BpsPercentRatioPattern2::new(client.clone(), _p("p2wsh", &acc)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4 {
|
||||
pub all: SeriesPattern1<StoredU64>,
|
||||
@@ -1551,6 +1595,17 @@ impl _1m1w1y24hBpsPercentRatioPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct ActiveInputOutputSpendablePattern {
|
||||
pub active_reused_addr_count: _1m1w1y24hBlockPattern,
|
||||
pub active_reused_addr_share: _1m1w1y24hBlockPattern2,
|
||||
pub input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6,
|
||||
pub input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7,
|
||||
pub output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6,
|
||||
pub output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7,
|
||||
pub spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CapLossMvrvNetPriceProfitSoprPattern {
|
||||
pub cap: CentsDeltaUsdPattern,
|
||||
@@ -1823,6 +1878,28 @@ impl DeltaDominanceHalfInTotalPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct _1m1w1y24hBlockPattern2 {
|
||||
pub _1m: SeriesPattern1<StoredF32>,
|
||||
pub _1w: SeriesPattern1<StoredF32>,
|
||||
pub _1y: SeriesPattern1<StoredF32>,
|
||||
pub _24h: SeriesPattern1<StoredF32>,
|
||||
pub block: SeriesPattern18<StoredF32>,
|
||||
}
|
||||
|
||||
impl _1m1w1y24hBlockPattern2 {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
_1m: SeriesPattern1::new(client.clone(), _m(&acc, "average_1m")),
|
||||
_1w: SeriesPattern1::new(client.clone(), _m(&acc, "average_1w")),
|
||||
_1y: SeriesPattern1::new(client.clone(), _m(&acc, "average_1y")),
|
||||
_24h: SeriesPattern1::new(client.clone(), _m(&acc, "average_24h")),
|
||||
block: SeriesPattern18::new(client.clone(), acc.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct _1m1w1y24hBlockPattern {
|
||||
pub _1m: SeriesPattern1<StoredF32>,
|
||||
@@ -2760,6 +2837,13 @@ impl CentsSatsUsdPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CountEventsSupplyPattern {
|
||||
pub count: FundedTotalPattern,
|
||||
pub events: ActiveInputOutputSpendablePattern,
|
||||
pub supply: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CumulativeRollingSumPattern {
|
||||
pub cumulative: SeriesPattern1<StoredU64>,
|
||||
@@ -4235,6 +4319,7 @@ pub struct SeriesTree_Addrs {
|
||||
pub total: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4,
|
||||
pub new: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6,
|
||||
pub reused: SeriesTree_Addrs_Reused,
|
||||
pub respent: SeriesTree_Addrs_Respent,
|
||||
pub exposed: SeriesTree_Addrs_Exposed,
|
||||
pub delta: SeriesTree_Addrs_Delta,
|
||||
pub avg_amount: SeriesTree_Addrs_AvgAmount,
|
||||
@@ -4252,6 +4337,7 @@ impl SeriesTree_Addrs {
|
||||
total: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4::new(client.clone(), "total_addr_count".to_string()),
|
||||
new: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "new_addr_count".to_string()),
|
||||
reused: SeriesTree_Addrs_Reused::new(client.clone(), format!("{base_path}_reused")),
|
||||
respent: SeriesTree_Addrs_Respent::new(client.clone(), format!("{base_path}_respent")),
|
||||
exposed: SeriesTree_Addrs_Exposed::new(client.clone(), format!("{base_path}_exposed")),
|
||||
delta: SeriesTree_Addrs_Delta::new(client.clone(), format!("{base_path}_delta")),
|
||||
avg_amount: SeriesTree_Addrs_AvgAmount::new(client.clone(), format!("{base_path}_avg_amount")),
|
||||
@@ -4506,6 +4592,7 @@ impl SeriesTree_Addrs_Activity_All {
|
||||
pub struct SeriesTree_Addrs_Reused {
|
||||
pub count: FundedTotalPattern,
|
||||
pub events: SeriesTree_Addrs_Reused_Events,
|
||||
pub supply: SeriesTree_Addrs_Reused_Supply,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Reused {
|
||||
@@ -4513,6 +4600,7 @@ impl SeriesTree_Addrs_Reused {
|
||||
Self {
|
||||
count: FundedTotalPattern::new(client.clone(), "reused_addr_count".to_string()),
|
||||
events: SeriesTree_Addrs_Reused_Events::new(client.clone(), format!("{base_path}_events")),
|
||||
supply: SeriesTree_Addrs_Reused_Supply::new(client.clone(), format!("{base_path}_supply")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4525,7 +4613,7 @@ pub struct SeriesTree_Addrs_Reused_Events {
|
||||
pub input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6,
|
||||
pub input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7,
|
||||
pub active_reused_addr_count: _1m1w1y24hBlockPattern,
|
||||
pub active_reused_addr_share: SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare,
|
||||
pub active_reused_addr_share: _1m1w1y24hBlockPattern2,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Reused_Events {
|
||||
@@ -4537,28 +4625,111 @@ impl SeriesTree_Addrs_Reused_Events {
|
||||
input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "input_from_reused_addr_count".to_string()),
|
||||
input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7::new(client.clone(), "input_from_reused_addr_share".to_string()),
|
||||
active_reused_addr_count: _1m1w1y24hBlockPattern::new(client.clone(), "active_reused_addr_count".to_string()),
|
||||
active_reused_addr_share: SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare::new(client.clone(), format!("{base_path}_active_reused_addr_share")),
|
||||
active_reused_addr_share: _1m1w1y24hBlockPattern2::new(client.clone(), "active_reused_addr_share".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare {
|
||||
pub block: SeriesPattern18<StoredF32>,
|
||||
pub _24h: SeriesPattern1<StoredF32>,
|
||||
pub _1w: SeriesPattern1<StoredF32>,
|
||||
pub _1m: SeriesPattern1<StoredF32>,
|
||||
pub _1y: SeriesPattern1<StoredF32>,
|
||||
pub struct SeriesTree_Addrs_Reused_Supply {
|
||||
pub all: BtcCentsSatsUsdPattern,
|
||||
pub p2pk65: BtcCentsSatsUsdPattern,
|
||||
pub p2pk33: BtcCentsSatsUsdPattern,
|
||||
pub p2pkh: BtcCentsSatsUsdPattern,
|
||||
pub p2sh: BtcCentsSatsUsdPattern,
|
||||
pub p2wpkh: BtcCentsSatsUsdPattern,
|
||||
pub p2wsh: BtcCentsSatsUsdPattern,
|
||||
pub p2tr: BtcCentsSatsUsdPattern,
|
||||
pub p2a: BtcCentsSatsUsdPattern,
|
||||
pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare {
|
||||
impl SeriesTree_Addrs_Reused_Supply {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
block: SeriesPattern18::new(client.clone(), "active_reused_addr_share".to_string()),
|
||||
_24h: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_24h".to_string()),
|
||||
_1w: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_1w".to_string()),
|
||||
_1m: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_1m".to_string()),
|
||||
_1y: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_1y".to_string()),
|
||||
all: BtcCentsSatsUsdPattern::new(client.clone(), "reused_addr_supply".to_string()),
|
||||
p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_reused_addr_supply".to_string()),
|
||||
p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_reused_addr_supply".to_string()),
|
||||
p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_reused_addr_supply".to_string()),
|
||||
p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_reused_addr_supply".to_string()),
|
||||
p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_reused_addr_supply".to_string()),
|
||||
p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_reused_addr_supply".to_string()),
|
||||
p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_reused_addr_supply".to_string()),
|
||||
p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_reused_addr_supply".to_string()),
|
||||
share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5::new(client.clone(), "reused_addr_supply_share".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Addrs_Respent {
|
||||
pub count: FundedTotalPattern,
|
||||
pub events: SeriesTree_Addrs_Respent_Events,
|
||||
pub supply: SeriesTree_Addrs_Respent_Supply,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Respent {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
count: FundedTotalPattern::new(client.clone(), "respent_addr_count".to_string()),
|
||||
events: SeriesTree_Addrs_Respent_Events::new(client.clone(), format!("{base_path}_events")),
|
||||
supply: SeriesTree_Addrs_Respent_Supply::new(client.clone(), format!("{base_path}_supply")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Addrs_Respent_Events {
|
||||
pub output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6,
|
||||
pub output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7,
|
||||
pub spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern,
|
||||
pub input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6,
|
||||
pub input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7,
|
||||
pub active_reused_addr_count: _1m1w1y24hBlockPattern,
|
||||
pub active_reused_addr_share: _1m1w1y24hBlockPattern2,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Respent_Events {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "output_to_respent_addr_count".to_string()),
|
||||
output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7::new(client.clone(), "output_to_respent_addr_share".to_string()),
|
||||
spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern::new(client.clone(), "spendable_output_to_respent_addr_share".to_string()),
|
||||
input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "input_from_respent_addr_count".to_string()),
|
||||
input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7::new(client.clone(), "input_from_respent_addr_share".to_string()),
|
||||
active_reused_addr_count: _1m1w1y24hBlockPattern::new(client.clone(), "active_respent_addr_count".to_string()),
|
||||
active_reused_addr_share: _1m1w1y24hBlockPattern2::new(client.clone(), "active_respent_addr_share".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Addrs_Respent_Supply {
|
||||
pub all: BtcCentsSatsUsdPattern,
|
||||
pub p2pk65: BtcCentsSatsUsdPattern,
|
||||
pub p2pk33: BtcCentsSatsUsdPattern,
|
||||
pub p2pkh: BtcCentsSatsUsdPattern,
|
||||
pub p2sh: BtcCentsSatsUsdPattern,
|
||||
pub p2wpkh: BtcCentsSatsUsdPattern,
|
||||
pub p2wsh: BtcCentsSatsUsdPattern,
|
||||
pub p2tr: BtcCentsSatsUsdPattern,
|
||||
pub p2a: BtcCentsSatsUsdPattern,
|
||||
pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Respent_Supply {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
all: BtcCentsSatsUsdPattern::new(client.clone(), "respent_addr_supply".to_string()),
|
||||
p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_respent_addr_supply".to_string()),
|
||||
p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_respent_addr_supply".to_string()),
|
||||
p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_respent_addr_supply".to_string()),
|
||||
p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_respent_addr_supply".to_string()),
|
||||
p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_respent_addr_supply".to_string()),
|
||||
p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_respent_addr_supply".to_string()),
|
||||
p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_respent_addr_supply".to_string()),
|
||||
p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_respent_addr_supply".to_string()),
|
||||
share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5::new(client.clone(), "respent_addr_supply_share".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4589,51 +4760,22 @@ pub struct SeriesTree_Addrs_Exposed_Supply {
|
||||
pub p2wsh: BtcCentsSatsUsdPattern,
|
||||
pub p2tr: BtcCentsSatsUsdPattern,
|
||||
pub p2a: BtcCentsSatsUsdPattern,
|
||||
pub share: SeriesTree_Addrs_Exposed_Supply_Share,
|
||||
pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Exposed_Supply {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
all: BtcCentsSatsUsdPattern::new(client.clone(), "exposed_supply".to_string()),
|
||||
p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_exposed_supply".to_string()),
|
||||
p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_exposed_supply".to_string()),
|
||||
p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_exposed_supply".to_string()),
|
||||
p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_exposed_supply".to_string()),
|
||||
p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_exposed_supply".to_string()),
|
||||
p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_exposed_supply".to_string()),
|
||||
p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_exposed_supply".to_string()),
|
||||
p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_exposed_supply".to_string()),
|
||||
share: SeriesTree_Addrs_Exposed_Supply_Share::new(client.clone(), format!("{base_path}_share")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Addrs_Exposed_Supply_Share {
|
||||
pub all: BpsPercentRatioPattern2,
|
||||
pub p2pk65: BpsPercentRatioPattern2,
|
||||
pub p2pk33: BpsPercentRatioPattern2,
|
||||
pub p2pkh: BpsPercentRatioPattern2,
|
||||
pub p2sh: BpsPercentRatioPattern2,
|
||||
pub p2wpkh: BpsPercentRatioPattern2,
|
||||
pub p2wsh: BpsPercentRatioPattern2,
|
||||
pub p2tr: BpsPercentRatioPattern2,
|
||||
pub p2a: BpsPercentRatioPattern2,
|
||||
}
|
||||
|
||||
impl SeriesTree_Addrs_Exposed_Supply_Share {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
all: BpsPercentRatioPattern2::new(client.clone(), "exposed_supply_share".to_string()),
|
||||
p2pk65: BpsPercentRatioPattern2::new(client.clone(), "p2pk65_exposed_supply_share".to_string()),
|
||||
p2pk33: BpsPercentRatioPattern2::new(client.clone(), "p2pk33_exposed_supply_share".to_string()),
|
||||
p2pkh: BpsPercentRatioPattern2::new(client.clone(), "p2pkh_exposed_supply_share".to_string()),
|
||||
p2sh: BpsPercentRatioPattern2::new(client.clone(), "p2sh_exposed_supply_share".to_string()),
|
||||
p2wpkh: BpsPercentRatioPattern2::new(client.clone(), "p2wpkh_exposed_supply_share".to_string()),
|
||||
p2wsh: BpsPercentRatioPattern2::new(client.clone(), "p2wsh_exposed_supply_share".to_string()),
|
||||
p2tr: BpsPercentRatioPattern2::new(client.clone(), "p2tr_exposed_supply_share".to_string()),
|
||||
p2a: BpsPercentRatioPattern2::new(client.clone(), "p2a_exposed_supply_share".to_string()),
|
||||
all: BtcCentsSatsUsdPattern::new(client.clone(), "exposed_addr_supply".to_string()),
|
||||
p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_exposed_addr_supply".to_string()),
|
||||
p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_exposed_addr_supply".to_string()),
|
||||
p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_exposed_addr_supply".to_string()),
|
||||
p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_exposed_addr_supply".to_string()),
|
||||
p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_exposed_addr_supply".to_string()),
|
||||
p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_exposed_addr_supply".to_string()),
|
||||
p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_exposed_addr_supply".to_string()),
|
||||
p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_exposed_addr_supply".to_string()),
|
||||
share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5::new(client.clone(), "exposed_addr_supply_share".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8755,7 +8897,7 @@ pub struct BrkClient {
|
||||
|
||||
impl BrkClient {
|
||||
/// Client version.
|
||||
pub const VERSION: &'static str = "v0.3.0-beta.3";
|
||||
pub const VERSION: &'static str = "v0.3.0-beta.4";
|
||||
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Indexes, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
use crate::{indexes, internal::PerBlock};
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AddrCountVecs<M: StorageMode = Rw>(#[traversable(flatten)] pub PerBlock<StoredU64, M>);
|
||||
|
||||
impl AddrCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(PerBlock::forced_import(db, name, version, indexes)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Address count per address type (runtime state).
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToAddrCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&AddrTypeToAddrCountVecs, Height)> for AddrTypeToAddrCount {
|
||||
#[inline]
|
||||
fn from((groups, starting_height): (&AddrTypeToAddrCountVecs, Height)) -> Self {
|
||||
if let Some(prev_height) = starting_height.decremented() {
|
||||
Self(ByAddrType {
|
||||
p2pk65: groups
|
||||
.p2pk65
|
||||
.height
|
||||
.collect_one(prev_height)
|
||||
.unwrap()
|
||||
.into(),
|
||||
p2pk33: groups
|
||||
.p2pk33
|
||||
.height
|
||||
.collect_one(prev_height)
|
||||
.unwrap()
|
||||
.into(),
|
||||
p2pkh: groups.p2pkh.height.collect_one(prev_height).unwrap().into(),
|
||||
p2sh: groups.p2sh.height.collect_one(prev_height).unwrap().into(),
|
||||
p2wpkh: groups
|
||||
.p2wpkh
|
||||
.height
|
||||
.collect_one(prev_height)
|
||||
.unwrap()
|
||||
.into(),
|
||||
p2wsh: groups.p2wsh.height.collect_one(prev_height).unwrap().into(),
|
||||
p2tr: groups.p2tr.height.collect_one(prev_height).unwrap().into(),
|
||||
p2a: groups.p2a.height.collect_one(prev_height).unwrap().into(),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Address count per address type, with height + derived indexes.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AddrTypeToAddrCountVecs<M: StorageMode = Rw>(ByAddrType<AddrCountVecs<M>>);
|
||||
|
||||
impl From<ByAddrType<AddrCountVecs>> for AddrTypeToAddrCountVecs {
|
||||
#[inline]
|
||||
fn from(value: ByAddrType<AddrCountVecs>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrTypeToAddrCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::from(ByAddrType::<AddrCountVecs>::new_with_name(
|
||||
|type_name| {
|
||||
AddrCountVecs::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
|
||||
},
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.0.values().map(|v| v.height.len()).min().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.0
|
||||
.par_values_mut()
|
||||
.map(|v| &mut v.height as &mut dyn AnyStoredVec)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, addr_counts: &AddrTypeToAddrCount) {
|
||||
for (vecs, &count) in self.0.values_mut().zip(addr_counts.values()) {
|
||||
vecs.height.push(count.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
for v in self.0.values_mut() {
|
||||
v.height.reset()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn by_height(&self) -> Vec<&EagerVec<PcoVec<Height, StoredU64>>> {
|
||||
self.0.values().map(|v| &v.height).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct AddrCountsVecs<M: StorageMode = Rw> {
|
||||
pub all: AddrCountVecs<M>,
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: AddrTypeToAddrCountVecs<M>,
|
||||
}
|
||||
|
||||
impl AddrCountsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
all: AddrCountVecs::forced_import(db, name, version, indexes)?,
|
||||
by_addr_type: AddrTypeToAddrCountVecs::forced_import(db, name, version, indexes)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.all
|
||||
.height
|
||||
.len()
|
||||
.min(self.by_addr_type.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
rayon::iter::once(&mut self.all.height as &mut dyn AnyStoredVec)
|
||||
.chain(self.by_addr_type.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.all.height.reset()?;
|
||||
self.by_addr_type.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, total: u64, addr_counts: &AddrTypeToAddrCount) {
|
||||
self.all.height.push(total.into());
|
||||
self.by_addr_type.push_height(addr_counts);
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
|
||||
let sources = self.by_addr_type.by_height();
|
||||
self.all
|
||||
.height
|
||||
.compute_sum_of_others(starting_indexes.height, &sources, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,17 @@ use crate::{
|
||||
internal::{PerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Reused address count (`all` + per-type) for a single variant (funded or total).
|
||||
use super::AddrTypeToAddrCount;
|
||||
|
||||
/// Per-block `StoredU64` counts with an aggregate `all` plus a per-address-type
|
||||
/// breakdown. Shared primitive backing addr-count, empty-addr-count, and the
|
||||
/// funded/total pairs used by exposed, reused, and respent.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ReusedAddrCountAllVecs<M: StorageMode = Rw>(
|
||||
pub struct AddrCountsVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
|
||||
);
|
||||
|
||||
impl ReusedAddrCountAllVecs {
|
||||
impl AddrCountsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
@@ -26,4 +30,9 @@ impl ReusedAddrCountAllVecs {
|
||||
db, name, version, indexes,
|
||||
)?))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_counts(&mut self, counts: &AddrTypeToAddrCount) {
|
||||
self.push_height(counts.sum(), counts.values().copied());
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,3 @@
|
||||
//! Exposed address count tracking — running counters of how many addresses
|
||||
//! are currently in (or have ever been in) the exposed set, per address type
|
||||
//! plus an aggregated `all`. See the parent [`super`] module for the
|
||||
//! definition of "exposed" and how it varies by address type.
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToExposedAddrCount;
|
||||
pub use vecs::ExposedAddrCountAllVecs;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Indexes, Version};
|
||||
@@ -17,29 +6,34 @@ use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::indexes;
|
||||
|
||||
/// Exposed address counts: funded (currently at-risk) and total (ever at-risk).
|
||||
use super::{AddrCountsVecs, AddrTypeToAddrCount};
|
||||
|
||||
/// Paired funded + cumulative-total address counts, used by exposed, reused,
|
||||
/// and respent. On-disk naming: `"{name}_addr_count"` (funded) and
|
||||
/// `"total_{name}_addr_count"` (total).
|
||||
#[derive(Traversable)]
|
||||
pub struct ExposedAddrCountsVecs<M: StorageMode = Rw> {
|
||||
pub funded: ExposedAddrCountAllVecs<M>,
|
||||
pub total: ExposedAddrCountAllVecs<M>,
|
||||
pub struct AddrCountFundedTotalVecs<M: StorageMode = Rw> {
|
||||
pub funded: AddrCountsVecs<M>,
|
||||
pub total: AddrCountsVecs<M>,
|
||||
}
|
||||
|
||||
impl ExposedAddrCountsVecs {
|
||||
impl AddrCountFundedTotalVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
funded: ExposedAddrCountAllVecs::forced_import(
|
||||
funded: AddrCountsVecs::forced_import(
|
||||
db,
|
||||
"exposed_addr_count",
|
||||
&format!("{name}_addr_count"),
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
total: ExposedAddrCountAllVecs::forced_import(
|
||||
total: AddrCountsVecs::forced_import(
|
||||
db,
|
||||
"total_exposed_addr_count",
|
||||
&format!("total_{name}_addr_count"),
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
@@ -66,6 +60,16 @@ impl ExposedAddrCountsVecs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_counts(
|
||||
&mut self,
|
||||
funded: &AddrTypeToAddrCount,
|
||||
total: &AddrTypeToAddrCount,
|
||||
) {
|
||||
self.funded.push_counts(funded);
|
||||
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)?;
|
||||
@@ -0,0 +1,7 @@
|
||||
mod all_vecs;
|
||||
mod funded_total_vecs;
|
||||
mod state;
|
||||
|
||||
pub use all_vecs::AddrCountsVecs;
|
||||
pub use funded_total_vecs::AddrCountFundedTotalVecs;
|
||||
pub use state::AddrTypeToAddrCount;
|
||||
@@ -0,0 +1,38 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::Height;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use super::AddrCountsVecs;
|
||||
|
||||
/// Per-addr-type address-count running total. Shared runtime state across
|
||||
/// funded / empty / exposed / reused / respent counters; paired with
|
||||
/// [`AddrCountsVecs`] on disk.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToAddrCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ByAddrType<u64>> for AddrTypeToAddrCount {
|
||||
#[inline]
|
||||
fn from(value: ByAddrType<u64>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&AddrCountsVecs, Height)> for AddrTypeToAddrCount {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&AddrCountsVecs, Height)) -> Self {
|
||||
let Some(prev_height) = starting_height.decremented() else {
|
||||
return Self::default();
|
||||
};
|
||||
vecs.by_addr_type
|
||||
.map_with_name(|_, v| v.height.collect_one(prev_height).unwrap().into())
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ impl DeltaVecs {
|
||||
let all = LazyRollingDeltasFromHeight::new(
|
||||
"addr_count",
|
||||
version,
|
||||
&addr_count.all.0.height,
|
||||
&addr_count.all.height,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
@@ -35,7 +35,7 @@ impl DeltaVecs {
|
||||
LazyRollingDeltasFromHeight::new(
|
||||
&format!("{name}_addr_count"),
|
||||
version,
|
||||
&addr.0.height,
|
||||
&addr.height,
|
||||
cached_starts,
|
||||
indexes,
|
||||
)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::{Height, StoredU64};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::internal::PerBlock;
|
||||
|
||||
use super::vecs::ExposedAddrCountAllVecs;
|
||||
|
||||
/// Runtime counter for exposed address counts per address type.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToExposedAddrCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToExposedAddrCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ExposedAddrCountAllVecs, Height)> for AddrTypeToExposedAddrCount {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ExposedAddrCountAllVecs, Height)) -> Self {
|
||||
if let Some(prev_height) = starting_height.decremented() {
|
||||
let read = |v: &PerBlock<StoredU64>| -> u64 {
|
||||
v.height.collect_one(prev_height).unwrap().into()
|
||||
};
|
||||
Self(ByAddrType {
|
||||
p2pk65: read(&vecs.by_addr_type.p2pk65),
|
||||
p2pk33: read(&vecs.by_addr_type.p2pk33),
|
||||
p2pkh: read(&vecs.by_addr_type.p2pkh),
|
||||
p2sh: read(&vecs.by_addr_type.p2sh),
|
||||
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
|
||||
p2wsh: read(&vecs.by_addr_type.p2wsh),
|
||||
p2tr: read(&vecs.by_addr_type.p2tr),
|
||||
p2a: read(&vecs.by_addr_type.p2a),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Exposed address count (`all` + per-type) for a single variant (funded or total).
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ExposedAddrCountAllVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
|
||||
);
|
||||
|
||||
impl ExposedAddrCountAllVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
|
||||
db, name, version, indexes,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
//! - **P2PKH, P2SH, P2WPKH, P2WSH**: the locking script contains a hash of
|
||||
//! the pubkey/script. The pubkey is only revealed when spending. Note that
|
||||
//! even the spending tx itself exposes the pubkey while the address still
|
||||
//! holds funds — during the mempool window between broadcast and confirmation,
|
||||
//! holds funds, during the mempool window between broadcast and confirmation,
|
||||
//! the pubkey is visible while the UTXO being spent is still unspent on-chain.
|
||||
//! So every spent address of these types has had at least one moment with
|
||||
//! funds at quantum risk.
|
||||
@@ -30,15 +30,9 @@
|
||||
//! sums these, giving "Bitcoin addresses currently with funds at quantum risk".
|
||||
//!
|
||||
//! All metrics are tracked as running counters and require no extra fields
|
||||
//! on the address data — they're maintained via delta detection in
|
||||
//! on the address data. They're maintained via delta detection in
|
||||
//! `process_received` and `process_sent`.
|
||||
|
||||
mod count;
|
||||
mod supply;
|
||||
|
||||
pub use count::{AddrTypeToExposedAddrCount, ExposedAddrCountsVecs};
|
||||
pub use supply::{AddrTypeToExposedSupply, ExposedAddrSupplyVecs, ExposedSupplyShareVecs};
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
@@ -46,16 +40,24 @@ use brk_types::{Height, Indexes, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{indexes, internal::RatioSatsBp16, prices};
|
||||
use super::{
|
||||
count::AddrCountFundedTotalVecs,
|
||||
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
|
||||
};
|
||||
use crate::{indexes, prices};
|
||||
|
||||
mod state;
|
||||
|
||||
pub use state::ExposedAddrState;
|
||||
|
||||
/// Top-level container for all exposed address tracking: counts (funded +
|
||||
/// total), the funded supply, and share of supply.
|
||||
#[derive(Traversable)]
|
||||
pub struct ExposedAddrVecs<M: StorageMode = Rw> {
|
||||
pub count: ExposedAddrCountsVecs<M>,
|
||||
pub supply: ExposedAddrSupplyVecs<M>,
|
||||
pub count: AddrCountFundedTotalVecs<M>,
|
||||
pub supply: AddrSupplyVecs<M>,
|
||||
#[traversable(wrap = "supply", rename = "share")]
|
||||
pub supply_share: ExposedSupplyShareVecs<M>,
|
||||
pub supply_share: AddrSupplyShareVecs<M>,
|
||||
}
|
||||
|
||||
impl ExposedAddrVecs {
|
||||
@@ -65,9 +67,9 @@ impl ExposedAddrVecs {
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: ExposedAddrCountsVecs::forced_import(db, version, indexes)?,
|
||||
supply: ExposedAddrSupplyVecs::forced_import(db, version, indexes)?,
|
||||
supply_share: ExposedSupplyShareVecs::forced_import(db, version, indexes)?,
|
||||
count: AddrCountFundedTotalVecs::forced_import(db, "exposed", version, indexes)?,
|
||||
supply: AddrSupplyVecs::forced_import(db, "exposed", version, indexes)?,
|
||||
supply_share: AddrSupplyShareVecs::forced_import(db, "exposed", version, indexes)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,6 +94,12 @@ impl ExposedAddrVecs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, state: &ExposedAddrState) {
|
||||
self.count.push_counts(&state.funded, &state.total);
|
||||
self.supply.push_supply(&state.supply);
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
@@ -103,32 +111,13 @@ impl ExposedAddrVecs {
|
||||
self.count.compute_rest(starting_indexes, exit)?;
|
||||
self.supply
|
||||
.compute_rest(starting_indexes.height, prices, exit)?;
|
||||
|
||||
let max_from = starting_indexes.height;
|
||||
|
||||
self.supply_share
|
||||
.all
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&self.supply.all.sats.height,
|
||||
all_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
for ((_, share), ((_, exposed), (_, denom))) in self
|
||||
.supply_share
|
||||
.by_addr_type
|
||||
.iter_mut()
|
||||
.zip(self.supply.by_addr_type.iter().zip(type_supply_sats.iter()))
|
||||
{
|
||||
share.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&exposed.sats.height,
|
||||
*denom,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
self.supply_share.compute_rest(
|
||||
starting_indexes.height,
|
||||
&self.supply,
|
||||
all_supply_sats,
|
||||
type_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use brk_types::{FundedAddrData, Height, OutputType};
|
||||
|
||||
use crate::distribution::{
|
||||
addr::{AddrReceivePreState, AddrSendPreState, AddrTypeToAddrCount, AddrTypeToSupply},
|
||||
block::TrackingStatus,
|
||||
};
|
||||
|
||||
use super::ExposedAddrVecs;
|
||||
|
||||
/// Runtime running totals for exposed-addr tracking. Mirrors the persistent
|
||||
/// fields of [`ExposedAddrVecs`]: funded count, total count, funded supply.
|
||||
/// Recovered from disk at the start of a block-loop run.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ExposedAddrState {
|
||||
pub funded: AddrTypeToAddrCount,
|
||||
pub total: AddrTypeToAddrCount,
|
||||
pub supply: AddrTypeToSupply,
|
||||
}
|
||||
|
||||
impl ExposedAddrState {
|
||||
/// Apply exposed-addr updates for a received output, AFTER the receive
|
||||
/// has mutated `addr_data`. `pre` is the snapshot taken before the mutation.
|
||||
#[inline]
|
||||
pub(crate) fn on_receive(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrReceivePreState,
|
||||
status: TrackingStatus,
|
||||
) {
|
||||
// Pubkey-exposure state is unchanged by a receive, so `pre.was_pubkey_exposed`
|
||||
// equals the post-receive value.
|
||||
if !pre.was_funded && pre.was_pubkey_exposed {
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
// Total for pk-exposed-at-funding types fires here on first receive.
|
||||
// Other types fire on first spend in [`Self::on_send`].
|
||||
if output_type.pubkey_exposed_at_funding() && matches!(status, TrackingStatus::New) {
|
||||
*self.total.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
let after = addr_data.exposed_supply_contribution(output_type);
|
||||
self.supply
|
||||
.apply_delta(output_type, pre.exposed_contribution, after);
|
||||
}
|
||||
|
||||
/// Apply exposed-addr updates for a spent UTXO, AFTER the send has mutated
|
||||
/// `addr_data`. `pre` is the snapshot taken before the mutation.
|
||||
#[inline]
|
||||
pub(crate) fn on_send(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrSendPreState,
|
||||
will_be_empty: bool,
|
||||
) {
|
||||
let after = addr_data.exposed_supply_contribution(output_type);
|
||||
self.supply
|
||||
.apply_delta(output_type, pre.exposed_contribution, after);
|
||||
// First-ever pubkey exposure. Non-pk-exposed types fire on first spend.
|
||||
// Pk-exposed types never fire here (was already exposed at first receive).
|
||||
if !pre.was_pubkey_exposed {
|
||||
*self.total.get_mut_unwrap(output_type) += 1;
|
||||
if !will_be_empty {
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
}
|
||||
// Leaving the funded exposed set: was in it iff pubkey was exposed pre-spend.
|
||||
if will_be_empty && pre.was_pubkey_exposed {
|
||||
*self.funded.get_mut_unwrap(output_type) -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ExposedAddrVecs, Height)> for ExposedAddrState {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ExposedAddrVecs, Height)) -> Self {
|
||||
Self {
|
||||
funded: AddrTypeToAddrCount::from((&vecs.count.funded, starting_height)),
|
||||
total: AddrTypeToAddrCount::from((&vecs.count.total, starting_height)),
|
||||
supply: AddrTypeToSupply::from((&vecs.supply, starting_height)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
//! Exposed address supply (sats) tracking — running sum of balances held by
|
||||
//! addresses currently in the funded exposed set, per address type plus an
|
||||
//! aggregated `all`. See the parent [`super`] module for the definition of
|
||||
//! "exposed" and how it varies by address type.
|
||||
|
||||
mod share;
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use share::ExposedSupplyShareVecs;
|
||||
pub use state::AddrTypeToExposedSupply;
|
||||
pub use vecs::ExposedAddrSupplyVecs;
|
||||
@@ -1,36 +0,0 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PercentPerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Share of exposed supply relative to total supply.
|
||||
///
|
||||
/// - `all`: exposed_supply / circulating_supply
|
||||
/// - Per-type: type's exposed_supply / type's total supply
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ExposedSupplyShareVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PercentPerBlock<BasisPoints16, M>>,
|
||||
);
|
||||
|
||||
impl ExposedSupplyShareVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(
|
||||
WithAddrTypes::<PercentPerBlock<BasisPoints16>>::forced_import(
|
||||
db,
|
||||
"exposed_supply_share",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::{Height, Sats};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::internal::ValuePerBlock;
|
||||
|
||||
use super::vecs::ExposedAddrSupplyVecs;
|
||||
|
||||
/// Runtime running counter for the total balance (sats) held by funded
|
||||
/// exposed addresses, per address type.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToExposedSupply(ByAddrType<Sats>);
|
||||
|
||||
impl AddrTypeToExposedSupply {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> Sats {
|
||||
self.0.values().copied().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ExposedAddrSupplyVecs, Height)> for AddrTypeToExposedSupply {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ExposedAddrSupplyVecs, Height)) -> Self {
|
||||
if let Some(prev_height) = starting_height.decremented() {
|
||||
let read =
|
||||
|v: &ValuePerBlock| -> Sats { v.sats.height.collect_one(prev_height).unwrap() };
|
||||
Self(ByAddrType {
|
||||
p2pk65: read(&vecs.by_addr_type.p2pk65),
|
||||
p2pk33: read(&vecs.by_addr_type.p2pk33),
|
||||
p2pkh: read(&vecs.by_addr_type.p2pkh),
|
||||
p2sh: read(&vecs.by_addr_type.p2sh),
|
||||
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
|
||||
p2wsh: read(&vecs.by_addr_type.p2wsh),
|
||||
p2tr: read(&vecs.by_addr_type.p2tr),
|
||||
p2a: read(&vecs.by_addr_type.p2a),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,25 @@
|
||||
mod activity;
|
||||
mod addr_count;
|
||||
mod count;
|
||||
mod data;
|
||||
mod delta;
|
||||
mod exposed;
|
||||
mod indexes;
|
||||
mod new_addr_count;
|
||||
mod reused;
|
||||
mod state;
|
||||
mod supply;
|
||||
mod total_addr_count;
|
||||
mod type_map;
|
||||
|
||||
pub use activity::{AddrActivityVecs, AddrTypeToActivityCounts};
|
||||
pub use addr_count::{AddrCountsVecs, AddrTypeToAddrCount};
|
||||
pub use count::{AddrCountsVecs, AddrTypeToAddrCount};
|
||||
pub use data::AddrsDataVecs;
|
||||
pub use delta::DeltaVecs;
|
||||
pub use exposed::{AddrTypeToExposedAddrCount, AddrTypeToExposedSupply, ExposedAddrVecs,};
|
||||
pub use exposed::{ExposedAddrState, ExposedAddrVecs};
|
||||
pub use indexes::AnyAddrIndexesVecs;
|
||||
pub use new_addr_count::NewAddrCountVecs;
|
||||
pub use reused::{AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, ReusedAddrVecs};
|
||||
pub use reused::{ReusedAddrState, ReusedAddrVecs};
|
||||
pub use state::{AddrMetricsState, AddrReceivePreState, AddrSendPreState};
|
||||
pub use supply::AddrTypeToSupply;
|
||||
pub use total_addr_count::TotalAddrCountVecs;
|
||||
pub use type_map::{AddrTypeToTypeIndexMap, AddrTypeToVec, HeightToAddrTypeToVec};
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
//! Reused address count tracking — running counters of how many addresses
|
||||
//! are currently in (or have ever been in) the reused set, per address type
|
||||
//! plus an aggregated `all`. See the parent [`super`] module for the
|
||||
//! definition of "reused".
|
||||
//!
|
||||
//! Two counters are exposed:
|
||||
//! - `funded`: addresses currently funded AND with `funded_txo_count > 1`
|
||||
//! - `total`: addresses that have ever satisfied `funded_txo_count > 1` (monotonic)
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToReusedAddrCount;
|
||||
pub use vecs::ReusedAddrCountAllVecs;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Indexes, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::indexes;
|
||||
|
||||
/// Reused address counts: funded (currently with balance) and total (ever reused).
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrCountsVecs<M: StorageMode = Rw> {
|
||||
pub funded: ReusedAddrCountAllVecs<M>,
|
||||
pub total: ReusedAddrCountAllVecs<M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrCountsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
funded: ReusedAddrCountAllVecs::forced_import(
|
||||
db,
|
||||
"reused_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
total: ReusedAddrCountAllVecs::forced_import(
|
||||
db,
|
||||
"total_reused_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.funded
|
||||
.min_stateful_len()
|
||||
.min(self.total.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.funded
|
||||
.par_iter_height_mut()
|
||||
.chain(self.total.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.funded.reset_height()?;
|
||||
self.total.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
|
||||
self.funded.compute_rest(starting_indexes, exit)?;
|
||||
self.total.compute_rest(starting_indexes, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::{Height, StoredU64};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::internal::PerBlock;
|
||||
|
||||
use super::vecs::ReusedAddrCountAllVecs;
|
||||
|
||||
/// Runtime counter for reused address counts per address type.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToReusedAddrCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToReusedAddrCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ReusedAddrCountAllVecs, Height)> for AddrTypeToReusedAddrCount {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ReusedAddrCountAllVecs, Height)) -> Self {
|
||||
if let Some(prev_height) = starting_height.decremented() {
|
||||
let read = |v: &PerBlock<StoredU64>| -> u64 {
|
||||
v.height.collect_one(prev_height).unwrap().into()
|
||||
};
|
||||
Self(ByAddrType {
|
||||
p2pk65: read(&vecs.by_addr_type.p2pk65),
|
||||
p2pk33: read(&vecs.by_addr_type.p2pk33),
|
||||
p2pkh: read(&vecs.by_addr_type.p2pkh),
|
||||
p2sh: read(&vecs.by_addr_type.p2sh),
|
||||
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
|
||||
p2wsh: read(&vecs.by_addr_type.p2wsh),
|
||||
p2tr: read(&vecs.by_addr_type.p2tr),
|
||||
p2a: read(&vecs.by_addr_type.p2a),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
//! Per-block reused-address event tracking. Holds both the output-side
|
||||
//! Per-block address-reuse event tracking. Holds both the output-side
|
||||
//! ("an output landed on a previously-used address") and input-side
|
||||
//! ("an input spent from an address in the reused set") event counters.
|
||||
//! See [`vecs::ReusedAddrEventsVecs`] for the full description of each
|
||||
//! metric.
|
||||
//! Shared between reused (receive-based) and respent (spend-based) flavors.
|
||||
//! See [`vecs::AddrEventsVecs`] for the full description of each metric.
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToReusedAddrEventCount;
|
||||
pub use vecs::ReusedAddrEventsVecs;
|
||||
pub use state::AddrTypeToAddrEventCount;
|
||||
pub use vecs::AddrEventsVecs;
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
/// Per-block running counter of reused-address events, per address type.
|
||||
/// Shared runtime container for both output-side events
|
||||
/// (`output_to_reused_addr_count`, outputs landing on addresses that
|
||||
/// had already received ≥ 1 prior output) and input-side events
|
||||
/// (`input_from_reused_addr_count`, inputs spending from addresses
|
||||
/// with lifetime `funded_txo_count > 1`). Reset at the start of each
|
||||
/// block (no disk recovery needed since per-block flow is
|
||||
/// reconstructed deterministically from `process_received` /
|
||||
/// `process_sent`).
|
||||
/// Per-block running counter of address-reuse events, per address type. Shared
|
||||
/// across reused (receive-based) and respent (spend-based) flavors, and
|
||||
/// across output-side ("output landed on a previously-used address") and
|
||||
/// input-side ("input spent from an address in the set") event kinds.
|
||||
///
|
||||
/// Reset at the start of each block; no disk recovery needed since per-block
|
||||
/// flow is reconstructed deterministically from `process_received` /
|
||||
/// `process_sent`.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToReusedAddrEventCount(ByAddrType<u64>);
|
||||
pub struct AddrTypeToAddrEventCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToReusedAddrEventCount {
|
||||
impl AddrTypeToAddrEventCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
outputs,
|
||||
};
|
||||
|
||||
use super::state::AddrTypeToReusedAddrEventCount;
|
||||
use super::state::AddrTypeToAddrEventCount;
|
||||
|
||||
/// Per-block reused-address event metrics. Holds three families of
|
||||
/// signals: output-level (use), input-level (spend), and address-level
|
||||
@@ -63,7 +63,7 @@ use super::state::AddrTypeToReusedAddrEventCount;
|
||||
/// distinct-address counts would be misleading because the same
|
||||
/// address can appear in multiple blocks.
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrEventsVecs<M: StorageMode = Rw> {
|
||||
pub struct AddrEventsVecs<M: StorageMode = Rw> {
|
||||
pub output_to_reused_addr_count:
|
||||
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
pub output_to_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
|
||||
@@ -75,9 +75,10 @@ pub struct ReusedAddrEventsVecs<M: StorageMode = Rw> {
|
||||
pub active_reused_addr_share: PerBlockRollingAverage<StoredF32, StoredF32, M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrEventsVecs {
|
||||
impl AddrEventsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
@@ -107,27 +108,31 @@ impl ReusedAddrEventsVecs {
|
||||
})
|
||||
};
|
||||
|
||||
let output_to_reused_addr_count = import_count("output_to_reused_addr_count")?;
|
||||
let output_to_reused_addr_share = import_percent("output_to_reused_addr_share")?;
|
||||
let output_to_reused_addr_count =
|
||||
import_count(&format!("output_to_{name}_addr_count"))?;
|
||||
let output_to_reused_addr_share =
|
||||
import_percent(&format!("output_to_{name}_addr_share"))?;
|
||||
let spendable_output_to_reused_addr_share = PercentCumulativeRolling::forced_import(
|
||||
db,
|
||||
"spendable_output_to_reused_addr_share",
|
||||
&format!("spendable_output_to_{name}_addr_share"),
|
||||
version,
|
||||
indexes,
|
||||
)?;
|
||||
let input_from_reused_addr_count = import_count("input_from_reused_addr_count")?;
|
||||
let input_from_reused_addr_share = import_percent("input_from_reused_addr_share")?;
|
||||
let input_from_reused_addr_count =
|
||||
import_count(&format!("input_from_{name}_addr_count"))?;
|
||||
let input_from_reused_addr_share =
|
||||
import_percent(&format!("input_from_{name}_addr_share"))?;
|
||||
|
||||
let active_reused_addr_count = PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
"active_reused_addr_count",
|
||||
&format!("active_{name}_addr_count"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
let active_reused_addr_share = PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
"active_reused_addr_share",
|
||||
&format!("active_{name}_addr_share"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
@@ -175,8 +180,8 @@ impl ReusedAddrEventsVecs {
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(
|
||||
&mut self,
|
||||
uses: &AddrTypeToReusedAddrEventCount,
|
||||
spends: &AddrTypeToReusedAddrEventCount,
|
||||
uses: &AddrTypeToAddrEventCount,
|
||||
spends: &AddrTypeToAddrEventCount,
|
||||
active_addr_count: u32,
|
||||
active_reused_addr_count: u32,
|
||||
) {
|
||||
|
||||
@@ -16,42 +16,56 @@
|
||||
//! paired with a percent over the matching block-level output/input
|
||||
//! total.
|
||||
|
||||
mod count;
|
||||
mod events;
|
||||
|
||||
pub use count::{AddrTypeToReusedAddrCount, ReusedAddrCountsVecs};
|
||||
pub use events::{AddrTypeToReusedAddrEventCount, ReusedAddrEventsVecs};
|
||||
pub use events::{AddrEventsVecs, AddrTypeToAddrEventCount};
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Indexes, Version};
|
||||
use brk_types::{Height, Indexes, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use super::{
|
||||
count::AddrCountFundedTotalVecs,
|
||||
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
|
||||
};
|
||||
use crate::{
|
||||
indexes, inputs,
|
||||
internal::{WindowStartVec, Windows},
|
||||
outputs,
|
||||
outputs, prices,
|
||||
};
|
||||
|
||||
mod state;
|
||||
|
||||
pub use state::ReusedAddrState;
|
||||
|
||||
/// Top-level container for all reused address tracking: counts (funded +
|
||||
/// total) plus per-block reuse events (output-side + input-side).
|
||||
/// total), per-block reuse events (output-side + input-side), and funded
|
||||
/// supply + share.
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrVecs<M: StorageMode = Rw> {
|
||||
pub count: ReusedAddrCountsVecs<M>,
|
||||
pub events: ReusedAddrEventsVecs<M>,
|
||||
pub count: AddrCountFundedTotalVecs<M>,
|
||||
pub events: AddrEventsVecs<M>,
|
||||
pub supply: AddrSupplyVecs<M>,
|
||||
#[traversable(wrap = "supply", rename = "share")]
|
||||
pub supply_share: AddrSupplyShareVecs<M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: ReusedAddrCountsVecs::forced_import(db, version, indexes)?,
|
||||
events: ReusedAddrEventsVecs::forced_import(db, version, indexes, cached_starts)?,
|
||||
count: AddrCountFundedTotalVecs::forced_import(db, name, version, indexes)?,
|
||||
events: AddrEventsVecs::forced_import(db, name, version, indexes, cached_starts)?,
|
||||
supply: AddrSupplyVecs::forced_import(db, name, version, indexes)?,
|
||||
supply_share: AddrSupplyShareVecs::forced_import(db, name, version, indexes)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,6 +73,7 @@ impl ReusedAddrVecs {
|
||||
self.count
|
||||
.min_stateful_len()
|
||||
.min(self.events.min_stateful_len())
|
||||
.min(self.supply.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
@@ -67,26 +82,50 @@ impl ReusedAddrVecs {
|
||||
self.count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.events.par_iter_height_mut())
|
||||
.chain(self.supply.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.count.reset_height()?;
|
||||
self.events.reset_height()?;
|
||||
self.supply.reset_height()?;
|
||||
self.supply_share.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, state: &ReusedAddrState, active_addr_count: u32) {
|
||||
self.count.push_counts(&state.funded, &state.total);
|
||||
self.supply.push_supply(&state.supply);
|
||||
self.events.push_height(
|
||||
&state.output_events,
|
||||
&state.input_events,
|
||||
active_addr_count,
|
||||
u32::try_from(state.active.sum()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
outputs_by_type: &outputs::ByTypeVecs,
|
||||
inputs_by_type: &inputs::ByTypeVecs,
|
||||
prices: &prices::Vecs,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(starting_indexes, exit)?;
|
||||
self.events.compute_rest(
|
||||
starting_indexes,
|
||||
outputs_by_type,
|
||||
inputs_by_type,
|
||||
self.events
|
||||
.compute_rest(starting_indexes, outputs_by_type, inputs_by_type, exit)?;
|
||||
self.supply
|
||||
.compute_rest(starting_indexes.height, prices, exit)?;
|
||||
self.supply_share.compute_rest(
|
||||
starting_indexes.height,
|
||||
&self.supply,
|
||||
all_supply_sats,
|
||||
type_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
use brk_types::{FundedAddrData, Height, OutputType};
|
||||
|
||||
use crate::distribution::addr::{
|
||||
AddrReceivePreState, AddrSendPreState, AddrTypeToAddrCount, AddrTypeToSupply,
|
||||
};
|
||||
|
||||
use super::{AddrTypeToAddrEventCount, ReusedAddrVecs};
|
||||
|
||||
/// Runtime state for receive-based (reused) or spend-based (respent)
|
||||
/// address tracking. Mirrors the persistent fields of [`ReusedAddrVecs`]
|
||||
/// (funded + total counts, funded supply) plus per-block event counters
|
||||
/// that reset every block.
|
||||
///
|
||||
/// `output_events`, `input_events`, and `active` are cleared via
|
||||
/// [`Self::reset_per_block`] at the start of each block. The three running
|
||||
/// totals (`funded`, `total`, `supply`) are recovered from disk at the start
|
||||
/// of a run via [`From<(&ReusedAddrVecs, Height)>`].
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ReusedAddrState {
|
||||
pub funded: AddrTypeToAddrCount,
|
||||
pub total: AddrTypeToAddrCount,
|
||||
pub supply: AddrTypeToSupply,
|
||||
pub output_events: AddrTypeToAddrEventCount,
|
||||
pub input_events: AddrTypeToAddrEventCount,
|
||||
pub active: AddrTypeToAddrEventCount,
|
||||
}
|
||||
|
||||
impl ReusedAddrState {
|
||||
#[inline]
|
||||
pub(crate) fn reset_per_block(&mut self) {
|
||||
self.output_events.reset();
|
||||
self.input_events.reset();
|
||||
self.active.reset();
|
||||
}
|
||||
|
||||
/// Apply reused-flavor (receive-based: `funded_txo_count > 1`) updates
|
||||
/// for a received output, AFTER the receive has mutated `addr_data`.
|
||||
#[inline]
|
||||
pub(crate) fn on_receive_as_reused(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrReceivePreState,
|
||||
output_count: u32,
|
||||
) {
|
||||
let is_now_reused = addr_data.is_reused();
|
||||
|
||||
// Threshold crossing: the 2nd lifetime receive lands here. The address
|
||||
// is always funded post-receive.
|
||||
if is_now_reused && !pre.was_reused {
|
||||
*self.total.get_mut_unwrap(output_type) += 1;
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
} else if pre.was_reused && !pre.was_funded {
|
||||
// Reactivation: already-reused address was empty, now funded.
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
|
||||
// output-to-reused events: outputs landing on addresses that had
|
||||
// already received >= 1 prior output, i.e. every output except the
|
||||
// very first one the address ever sees. With `before =
|
||||
// prev_funded_txo_count` and `N = output_count`: events = N - max(0, 1 - before).
|
||||
let skip_first = 1u32.saturating_sub(pre.prev_funded_txo_count.min(1));
|
||||
let reused_events = output_count.saturating_sub(skip_first);
|
||||
if reused_events > 0 {
|
||||
*self.output_events.get_mut_unwrap(output_type) += u64::from(reused_events);
|
||||
}
|
||||
|
||||
if is_now_reused {
|
||||
*self.active.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
|
||||
let after = addr_data.reused_supply_contribution();
|
||||
self.supply
|
||||
.apply_delta(output_type, pre.reused_contribution, after);
|
||||
}
|
||||
|
||||
/// Apply respent-flavor (spend-based: `spent_txo_count > 1`) updates for a
|
||||
/// received output, AFTER the receive has mutated `addr_data`. Receives
|
||||
/// don't cross the respent threshold. The only transition is an
|
||||
/// already-respent empty address reactivating into the funded set.
|
||||
#[inline]
|
||||
pub(crate) fn on_receive_as_respent(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrReceivePreState,
|
||||
output_count: u32,
|
||||
) {
|
||||
if pre.was_respent && !pre.was_funded {
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
// Respent status is stable across a receive, so every output lands on
|
||||
// a respent address iff the address was already respent.
|
||||
if pre.was_respent {
|
||||
*self.output_events.get_mut_unwrap(output_type) += u64::from(output_count);
|
||||
*self.active.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
let after = addr_data.respent_supply_contribution();
|
||||
self.supply
|
||||
.apply_delta(output_type, pre.respent_contribution, after);
|
||||
}
|
||||
|
||||
/// Apply reused-flavor updates for a spent UTXO, AFTER the send has
|
||||
/// mutated `addr_data`. Sends don't change the reused predicate, so
|
||||
/// `pre.was_reused == is_reused` post-spend.
|
||||
#[inline]
|
||||
pub(crate) fn on_send_as_reused(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrSendPreState,
|
||||
is_first_encounter: bool,
|
||||
also_received: bool,
|
||||
will_be_empty: bool,
|
||||
) {
|
||||
if pre.was_reused {
|
||||
*self.input_events.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
// Active reused: first-encounter sender, currently reused, and not
|
||||
// already counted on the receive side.
|
||||
if is_first_encounter && pre.was_reused && !also_received {
|
||||
*self.active.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
if will_be_empty && pre.was_reused {
|
||||
*self.funded.get_mut_unwrap(output_type) -= 1;
|
||||
}
|
||||
let after = addr_data.reused_supply_contribution();
|
||||
self.supply
|
||||
.apply_delta(output_type, pre.reused_contribution, after);
|
||||
}
|
||||
|
||||
/// Apply respent-flavor updates for a spent UTXO, AFTER the send has
|
||||
/// mutated `addr_data`. Sends CAN cross the respent threshold on the
|
||||
/// 2nd lifetime spend.
|
||||
#[inline]
|
||||
pub(crate) fn on_send_as_respent(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrSendPreState,
|
||||
is_first_encounter: bool,
|
||||
also_received: bool,
|
||||
will_be_empty: bool,
|
||||
) {
|
||||
if pre.was_respent {
|
||||
*self.input_events.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
|
||||
let is_now_respent = addr_data.is_respent();
|
||||
|
||||
// Threshold crossing: the 2nd spend ever on this address. Always
|
||||
// bumps the monotonic total. Bumps the funded count iff the address
|
||||
// still has a balance. If the crossing spend also empties the
|
||||
// address, the `will_be_empty` branch below doesn't decrement
|
||||
// (was_respent is false), so the funded count stays correct.
|
||||
if is_now_respent && !pre.was_respent {
|
||||
*self.total.get_mut_unwrap(output_type) += 1;
|
||||
if !will_be_empty {
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Active respent splits cleanly into two disjoint branches (gated on
|
||||
// `pre.was_respent`):
|
||||
// - was already respent + active this block, and not also counted
|
||||
// on the receive side: pure senders on first spend.
|
||||
// - crosses the respent threshold this block: fires once per
|
||||
// address ever, on the exact crossing spend.
|
||||
if (is_first_encounter && pre.was_respent && !also_received)
|
||||
|| (is_now_respent && !pre.was_respent)
|
||||
{
|
||||
*self.active.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
|
||||
// Leaving the funded respent set on empty uses pre-spend state: a
|
||||
// threshold-crossing spend that also empties the address never
|
||||
// entered the funded set above (gated on !will_be_empty), so we
|
||||
// don't double-decrement.
|
||||
if will_be_empty && pre.was_respent {
|
||||
*self.funded.get_mut_unwrap(output_type) -= 1;
|
||||
}
|
||||
|
||||
let after = addr_data.respent_supply_contribution();
|
||||
self.supply
|
||||
.apply_delta(output_type, pre.respent_contribution, after);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ReusedAddrVecs, Height)> for ReusedAddrState {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ReusedAddrVecs, Height)) -> Self {
|
||||
Self {
|
||||
funded: AddrTypeToAddrCount::from((&vecs.count.funded, starting_height)),
|
||||
total: AddrTypeToAddrCount::from((&vecs.count.total, starting_height)),
|
||||
supply: AddrTypeToSupply::from((&vecs.supply, starting_height)),
|
||||
output_events: AddrTypeToAddrEventCount::default(),
|
||||
input_events: AddrTypeToAddrEventCount::default(),
|
||||
active: AddrTypeToAddrEventCount::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
use brk_types::{FundedAddrData, Height, OutputType, Sats};
|
||||
|
||||
use crate::distribution::{block::TrackingStatus, vecs::AddrMetricsVecs};
|
||||
|
||||
use super::{
|
||||
AddrTypeToActivityCounts, AddrTypeToAddrCount, ExposedAddrState, ReusedAddrState,
|
||||
};
|
||||
|
||||
/// Bundle of per-block runtime state for the full address-metrics pipeline.
|
||||
/// Feeds `process_received` / `process_sent` and is pushed to [`AddrMetricsVecs`]
|
||||
/// once per block.
|
||||
///
|
||||
/// Recovery: [`From<(&AddrMetricsVecs, Height)>`] reads the prior block from
|
||||
/// disk to seed all persistent running totals. Per-block counters (activity,
|
||||
/// and event counts inside each [`ReusedAddrState`]) default to zero and are
|
||||
/// cleared at the top of each block via [`Self::reset_per_block`].
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AddrMetricsState {
|
||||
pub funded: AddrTypeToAddrCount,
|
||||
pub empty: AddrTypeToAddrCount,
|
||||
pub activity: AddrTypeToActivityCounts,
|
||||
pub reused: ReusedAddrState,
|
||||
pub respent: ReusedAddrState,
|
||||
pub exposed: ExposedAddrState,
|
||||
}
|
||||
|
||||
/// Snapshot of [`FundedAddrData`] taken BEFORE a receive mutates it.
|
||||
/// Feeds delta-based updates in [`AddrMetricsState::on_receive_applied`].
|
||||
#[derive(Debug)]
|
||||
pub struct AddrReceivePreState {
|
||||
pub was_funded: bool,
|
||||
pub was_reused: bool,
|
||||
pub was_respent: bool,
|
||||
pub was_pubkey_exposed: bool,
|
||||
pub prev_funded_txo_count: u32,
|
||||
pub exposed_contribution: Sats,
|
||||
pub reused_contribution: Sats,
|
||||
pub respent_contribution: Sats,
|
||||
}
|
||||
|
||||
impl AddrReceivePreState {
|
||||
#[inline]
|
||||
pub fn capture(addr_data: &FundedAddrData, output_type: OutputType) -> Self {
|
||||
Self {
|
||||
was_funded: addr_data.is_funded(),
|
||||
was_reused: addr_data.is_reused(),
|
||||
was_respent: addr_data.is_respent(),
|
||||
was_pubkey_exposed: addr_data.is_pubkey_exposed(output_type),
|
||||
prev_funded_txo_count: addr_data.funded_txo_count,
|
||||
exposed_contribution: addr_data.exposed_supply_contribution(output_type),
|
||||
reused_contribution: addr_data.reused_supply_contribution(),
|
||||
respent_contribution: addr_data.respent_supply_contribution(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot of [`FundedAddrData`] taken BEFORE a spend mutates it.
|
||||
/// Feeds delta-based updates in [`AddrMetricsState::on_send_applied`].
|
||||
#[derive(Debug)]
|
||||
pub struct AddrSendPreState {
|
||||
pub was_reused: bool,
|
||||
pub was_respent: bool,
|
||||
pub was_pubkey_exposed: bool,
|
||||
pub exposed_contribution: Sats,
|
||||
pub reused_contribution: Sats,
|
||||
pub respent_contribution: Sats,
|
||||
}
|
||||
|
||||
impl AddrSendPreState {
|
||||
#[inline]
|
||||
pub fn capture(addr_data: &FundedAddrData, output_type: OutputType) -> Self {
|
||||
Self {
|
||||
was_reused: addr_data.is_reused(),
|
||||
was_respent: addr_data.is_respent(),
|
||||
was_pubkey_exposed: addr_data.is_pubkey_exposed(output_type),
|
||||
exposed_contribution: addr_data.exposed_supply_contribution(output_type),
|
||||
reused_contribution: addr_data.reused_supply_contribution(),
|
||||
respent_contribution: addr_data.respent_supply_contribution(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrMetricsState {
|
||||
#[inline]
|
||||
pub(crate) fn reset_per_block(&mut self) {
|
||||
self.activity.reset();
|
||||
self.reused.reset_per_block();
|
||||
self.respent.reset_per_block();
|
||||
}
|
||||
|
||||
/// Apply all state updates for a received output, AFTER the cohort and
|
||||
/// `addr_data` have been mutated. `pre` is the snapshot captured before
|
||||
/// the mutation, `addr_data` is the post-receive view.
|
||||
#[inline]
|
||||
pub(crate) fn on_receive_applied(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
status: TrackingStatus,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrReceivePreState,
|
||||
output_count: u32,
|
||||
) {
|
||||
let activity = self.activity.get_mut_unwrap(output_type);
|
||||
activity.receiving += 1;
|
||||
match status {
|
||||
TrackingStatus::New => {
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
TrackingStatus::WasEmpty => {
|
||||
activity.reactivated += 1;
|
||||
*self.funded.get_mut_unwrap(output_type) += 1;
|
||||
*self.empty.get_mut_unwrap(output_type) -= 1;
|
||||
}
|
||||
TrackingStatus::Tracked => {}
|
||||
}
|
||||
self.reused
|
||||
.on_receive_as_reused(output_type, addr_data, pre, output_count);
|
||||
self.respent
|
||||
.on_receive_as_respent(output_type, addr_data, pre, output_count);
|
||||
self.exposed.on_receive(output_type, addr_data, pre, status);
|
||||
}
|
||||
|
||||
/// Apply all state updates for a spent UTXO, AFTER the cohort and
|
||||
/// `addr_data` have been mutated. `pre` is the snapshot captured before
|
||||
/// the mutation. `is_first_encounter` / `also_received` come from the
|
||||
/// caller's per-block seen/received tracking. `will_be_empty` is from
|
||||
/// the pre-mutation `addr_data.has_1_utxos()`.
|
||||
#[inline]
|
||||
pub(crate) fn on_send_applied(
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
addr_data: &FundedAddrData,
|
||||
pre: &AddrSendPreState,
|
||||
is_first_encounter: bool,
|
||||
also_received: bool,
|
||||
will_be_empty: bool,
|
||||
) {
|
||||
if is_first_encounter {
|
||||
let activity = self.activity.get_mut_unwrap(output_type);
|
||||
activity.sending += 1;
|
||||
if also_received {
|
||||
activity.bidirectional += 1;
|
||||
}
|
||||
}
|
||||
if will_be_empty {
|
||||
*self.funded.get_mut_unwrap(output_type) -= 1;
|
||||
*self.empty.get_mut_unwrap(output_type) += 1;
|
||||
}
|
||||
self.reused.on_send_as_reused(
|
||||
output_type,
|
||||
addr_data,
|
||||
pre,
|
||||
is_first_encounter,
|
||||
also_received,
|
||||
will_be_empty,
|
||||
);
|
||||
self.respent.on_send_as_respent(
|
||||
output_type,
|
||||
addr_data,
|
||||
pre,
|
||||
is_first_encounter,
|
||||
also_received,
|
||||
will_be_empty,
|
||||
);
|
||||
self.exposed.on_send(output_type, addr_data, pre, will_be_empty);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&AddrMetricsVecs, Height)> for AddrMetricsState {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&AddrMetricsVecs, Height)) -> Self {
|
||||
Self {
|
||||
funded: AddrTypeToAddrCount::from((&vecs.funded, starting_height)),
|
||||
empty: AddrTypeToAddrCount::from((&vecs.empty, starting_height)),
|
||||
activity: AddrTypeToActivityCounts::default(),
|
||||
reused: ReusedAddrState::from((&vecs.reused, starting_height)),
|
||||
respent: ReusedAddrState::from((&vecs.respent, starting_height)),
|
||||
exposed: ExposedAddrState::from((&vecs.exposed, starting_height)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Generic per-address-type supply tracking, shared across predicate-based
|
||||
//! supply categories (exposed, reused, respent). A "category supply" is the
|
||||
//! running sum of balances held by addresses currently in the funded subset
|
||||
//! defined by some predicate.
|
||||
|
||||
mod share;
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use share::AddrSupplyShareVecs;
|
||||
pub use state::AddrTypeToSupply;
|
||||
pub use vecs::AddrSupplyVecs;
|
||||
@@ -0,0 +1,69 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Height, Sats, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PercentPerBlock, RatioSatsBp16, WithAddrTypes},
|
||||
};
|
||||
|
||||
use super::vecs::AddrSupplyVecs;
|
||||
|
||||
/// Share of a predicate-based supply category relative to total supply.
|
||||
///
|
||||
/// - `all`: category supply / circulating supply
|
||||
/// - Per-type: type's category supply / type's total supply
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AddrSupplyShareVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PercentPerBlock<BasisPoints16, M>>,
|
||||
);
|
||||
|
||||
impl AddrSupplyShareVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(
|
||||
WithAddrTypes::<PercentPerBlock<BasisPoints16>>::forced_import(
|
||||
db,
|
||||
&format!("{name}_addr_supply_share"),
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
supply: &AddrSupplyVecs,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.all.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&supply.all.sats.height,
|
||||
all_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
for ((_, share), ((_, cat), (_, denom))) in self
|
||||
.by_addr_type
|
||||
.iter_mut()
|
||||
.zip(supply.by_addr_type.iter().zip(type_supply_sats.iter()))
|
||||
{
|
||||
share.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&cat.sats.height,
|
||||
*denom,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::{Height, OutputType, Sats};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use super::vecs::AddrSupplyVecs;
|
||||
|
||||
/// Per-addr-type running-total of a supply category (sats). Shared across
|
||||
/// predicate-based supply categories (exposed, reused, respent).
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToSupply(ByAddrType<Sats>);
|
||||
|
||||
impl AddrTypeToSupply {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> Sats {
|
||||
self.0.values().copied().sum()
|
||||
}
|
||||
|
||||
/// Apply a signed `after - before` delta to the slot for `output_type`.
|
||||
/// Sats is unsigned, so branch on sign.
|
||||
#[inline]
|
||||
pub(crate) fn apply_delta(&mut self, output_type: OutputType, before: Sats, after: Sats) {
|
||||
let slot = self.get_mut_unwrap(output_type);
|
||||
if after >= before {
|
||||
*slot += after - before;
|
||||
} else {
|
||||
*slot -= before - after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ByAddrType<Sats>> for AddrTypeToSupply {
|
||||
#[inline]
|
||||
fn from(value: ByAddrType<Sats>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&AddrSupplyVecs, Height)> for AddrTypeToSupply {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&AddrSupplyVecs, Height)) -> Self {
|
||||
let Some(prev_height) = starting_height.decremented() else {
|
||||
return Self::default();
|
||||
};
|
||||
vecs.by_addr_type
|
||||
.map_with_name(|_, v| v.sats.height.collect_one(prev_height).unwrap())
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -9,26 +9,34 @@ use crate::{
|
||||
internal::{ValuePerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Exposed address supply (sats/btc/cents/usd) — `all` + per-address-type.
|
||||
/// Tracks the total balance held by addresses currently in the funded
|
||||
/// exposed set. Sats are pushed stateful per block; cents/usd are derived
|
||||
/// post-hoc from sats × spot price.
|
||||
use super::AddrTypeToSupply;
|
||||
|
||||
/// Per-addr-type running supply (sats/btc/cents/usd) with an aggregated `all`.
|
||||
/// Shared across predicate-based supply categories (exposed, reused, respent).
|
||||
/// Sats are pushed stateful per block; cents/usd are derived post-hoc from
|
||||
/// sats × spot price.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ExposedAddrSupplyVecs<M: StorageMode = Rw>(
|
||||
pub struct AddrSupplyVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<ValuePerBlock<M>>,
|
||||
);
|
||||
|
||||
impl ExposedAddrSupplyVecs {
|
||||
impl AddrSupplyVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<ValuePerBlock>::forced_import(
|
||||
db,
|
||||
"exposed_supply",
|
||||
&format!("{name}_addr_supply"),
|
||||
version,
|
||||
indexes,
|
||||
)?))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_supply(&mut self, supply: &AddrTypeToSupply) {
|
||||
self.push_height(supply.sum(), supply.values().copied());
|
||||
}
|
||||
}
|
||||
@@ -39,14 +39,14 @@ impl TotalAddrCountVecs {
|
||||
empty_addr_count: &AddrCountsVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.0.all.height.compute_add(
|
||||
self.all.height.compute_add(
|
||||
max_from,
|
||||
&addr_count.all.height,
|
||||
&empty_addr_count.all.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
for ((_, total), ((_, addr), (_, empty))) in self.0.by_addr_type.iter_mut().zip(
|
||||
for ((_, total), ((_, addr), (_, empty))) in self.by_addr_type.iter_mut().zip(
|
||||
addr_count
|
||||
.by_addr_type
|
||||
.iter()
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use brk_cohort::{AmountBucket, ByAddrType};
|
||||
use brk_cohort::AmountBucket;
|
||||
use brk_types::{Cents, Sats, TypeIndex};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::distribution::{
|
||||
addr::{
|
||||
AddrTypeToActivityCounts, AddrTypeToExposedAddrCount, AddrTypeToExposedSupply,
|
||||
AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, AddrTypeToVec,
|
||||
},
|
||||
addr::{AddrMetricsState, AddrReceivePreState, AddrTypeToVec},
|
||||
cohorts::AddrCohorts,
|
||||
};
|
||||
|
||||
@@ -19,22 +16,12 @@ struct AggregatedReceive {
|
||||
output_count: u32,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn process_received(
|
||||
received_data: AddrTypeToVec<(TypeIndex, Sats)>,
|
||||
cohorts: &mut AddrCohorts,
|
||||
lookup: &mut AddrLookup<'_>,
|
||||
price: Cents,
|
||||
addr_count: &mut ByAddrType<u64>,
|
||||
empty_addr_count: &mut ByAddrType<u64>,
|
||||
activity_counts: &mut AddrTypeToActivityCounts,
|
||||
reused_addr_count: &mut AddrTypeToReusedAddrCount,
|
||||
total_reused_addr_count: &mut AddrTypeToReusedAddrCount,
|
||||
output_to_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
active_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
total_exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
exposed_supply: &mut AddrTypeToExposedSupply,
|
||||
state: &mut AddrMetricsState,
|
||||
) {
|
||||
let max_type_len = received_data
|
||||
.iter()
|
||||
@@ -49,19 +36,7 @@ pub(crate) fn process_received(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cache mutable refs for this address type
|
||||
let type_addr_count = addr_count.get_mut(output_type).unwrap();
|
||||
let type_empty_count = empty_addr_count.get_mut(output_type).unwrap();
|
||||
let type_activity = activity_counts.get_mut_unwrap(output_type);
|
||||
let type_reused_count = reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_total_reused_count = total_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_output_to_reused_count = output_to_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_active_reused_count = active_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_count = exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_total_exposed_count = total_exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_supply = exposed_supply.get_mut(output_type).unwrap();
|
||||
|
||||
// Aggregate receives by address - each address processed exactly once
|
||||
// Aggregate per address so each address is processed exactly once.
|
||||
for (type_index, value) in vec {
|
||||
let entry = aggregated.entry(type_index).or_default();
|
||||
entry.total_value += value;
|
||||
@@ -70,39 +45,13 @@ pub(crate) fn process_received(
|
||||
|
||||
for (type_index, recv) in aggregated.drain() {
|
||||
let (addr_data, status) = lookup.get_or_create_for_receive(output_type, type_index);
|
||||
let pre = AddrReceivePreState::capture(addr_data, output_type);
|
||||
|
||||
// Track receiving activity - each address in receive aggregation
|
||||
type_activity.receiving += 1;
|
||||
|
||||
// Capture state BEFORE the receive mutates funded_txo_count
|
||||
let was_funded = addr_data.is_funded();
|
||||
let was_reused = addr_data.is_reused();
|
||||
let funded_txo_count_before = addr_data.funded_txo_count;
|
||||
let was_pubkey_exposed = addr_data.is_pubkey_exposed(output_type);
|
||||
let exposed_contribution_before = addr_data.exposed_supply_contribution(output_type);
|
||||
|
||||
match status {
|
||||
TrackingStatus::New => {
|
||||
*type_addr_count += 1;
|
||||
}
|
||||
TrackingStatus::WasEmpty => {
|
||||
*type_addr_count += 1;
|
||||
*type_empty_count -= 1;
|
||||
// Reactivated - was empty, now has funds
|
||||
type_activity.reactivated += 1;
|
||||
}
|
||||
TrackingStatus::Tracked => {}
|
||||
}
|
||||
|
||||
let is_new_entry = matches!(status, TrackingStatus::New | TrackingStatus::WasEmpty);
|
||||
|
||||
if is_new_entry {
|
||||
// New/was-empty address - just add to cohort
|
||||
if matches!(status, TrackingStatus::New | TrackingStatus::WasEmpty) {
|
||||
addr_data.receive_outputs(recv.total_value, price, recv.output_count);
|
||||
let new_bucket = AmountBucket::from(recv.total_value);
|
||||
cohorts
|
||||
.amount_range
|
||||
.get_mut_by_bucket(new_bucket)
|
||||
.get_mut_by_bucket(AmountBucket::from(recv.total_value))
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
@@ -114,7 +63,6 @@ pub(crate) fn process_received(
|
||||
let new_bucket = AmountBucket::from(new_balance);
|
||||
|
||||
if let Some((old_bucket, new_bucket)) = prev_bucket.transition_to(new_bucket) {
|
||||
// Crossing cohort boundary - subtract from old, add to new
|
||||
let cohort_state = cohorts
|
||||
.amount_range
|
||||
.get_mut_by_bucket(old_bucket)
|
||||
@@ -122,7 +70,6 @@ pub(crate) fn process_received(
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
|
||||
// Debug info for tracking down underflow issues
|
||||
if cohort_state.inner.supply.utxo_count < addr_data.utxo_count() as u64 {
|
||||
panic!(
|
||||
"process_received: cohort underflow detected!\n\
|
||||
@@ -148,7 +95,6 @@ pub(crate) fn process_received(
|
||||
.unwrap()
|
||||
.add(addr_data);
|
||||
} else {
|
||||
// Staying in same cohort - just receive
|
||||
cohorts
|
||||
.amount_range
|
||||
.get_mut_by_bucket(new_bucket)
|
||||
@@ -159,61 +105,7 @@ pub(crate) fn process_received(
|
||||
}
|
||||
}
|
||||
|
||||
// Update reused counts based on the post-receive state
|
||||
let is_now_reused = addr_data.is_reused();
|
||||
if is_now_reused && !was_reused {
|
||||
// Newly crossed the reuse threshold this block
|
||||
*type_reused_count += 1;
|
||||
*type_total_reused_count += 1;
|
||||
} else if is_now_reused && !was_funded {
|
||||
// Already-reused address reactivating into the funded set
|
||||
*type_reused_count += 1;
|
||||
}
|
||||
|
||||
// Block-level "active reused address" count: each address
|
||||
// is processed exactly once here (via aggregation), so we
|
||||
// count it once iff it is reused after the block's receives.
|
||||
// The sender-side counterpart in process_sent dedupes
|
||||
// against `received_addrs` so addresses that did both
|
||||
// aren't double-counted.
|
||||
if is_now_reused {
|
||||
*type_active_reused_count += 1;
|
||||
}
|
||||
|
||||
// Per-block reused-use count: every individual output to this
|
||||
// address counts iff, at the moment the output arrives, the
|
||||
// address had already received at least one prior output
|
||||
// (i.e. it is an output-level "address reuse event"). With
|
||||
// aggregation, that means we skip the very first output the
|
||||
// address ever sees and count every subsequent one, so
|
||||
// `skipped` is `max(0, 1 - before)`.
|
||||
let skipped = 1u32.saturating_sub(funded_txo_count_before);
|
||||
let counted = recv.output_count.saturating_sub(skipped);
|
||||
*type_output_to_reused_count += u64::from(counted);
|
||||
|
||||
// Update exposed counts. The address's pubkey-exposure state
|
||||
// is unchanged by a receive (spent_txo_count unchanged), so we
|
||||
// can use the captured `was_pubkey_exposed` for both pre and post.
|
||||
// After the receive the address is always funded, so it's in the
|
||||
// funded exposed set iff its pubkey is exposed.
|
||||
//
|
||||
// Funded exposed enters when the address wasn't funded before but
|
||||
// is now AND its pubkey is exposed.
|
||||
// Total exposed (pk_exposed_at_funding types only) increments on
|
||||
// first-ever receive (status == TrackingStatus::New); for other
|
||||
// types it's incremented in process_sent on the first spend.
|
||||
if !was_funded && was_pubkey_exposed {
|
||||
*type_exposed_count += 1;
|
||||
}
|
||||
if output_type.pubkey_exposed_at_funding() && matches!(status, TrackingStatus::New) {
|
||||
*type_total_exposed_count += 1;
|
||||
}
|
||||
|
||||
// Update exposed supply via post-receive contribution delta.
|
||||
let exposed_contribution_after = addr_data.exposed_supply_contribution(output_type);
|
||||
// Receives can only add to balance and membership, so the delta
|
||||
// is always non-negative.
|
||||
*type_exposed_supply += exposed_contribution_after - exposed_contribution_before;
|
||||
state.on_receive_applied(output_type, status, addr_data, &pre, recv.output_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,20 @@ use rustc_hash::FxHashSet;
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::distribution::{
|
||||
addr::{
|
||||
AddrTypeToActivityCounts, AddrTypeToExposedAddrCount, AddrTypeToExposedSupply,
|
||||
AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, HeightToAddrTypeToVec,
|
||||
},
|
||||
addr::{AddrMetricsState, AddrSendPreState, HeightToAddrTypeToVec},
|
||||
cohorts::AddrCohorts,
|
||||
compute::PriceRangeMax,
|
||||
};
|
||||
|
||||
use super::super::cache::AddrLookup;
|
||||
|
||||
/// Process sent outputs for address cohorts.
|
||||
/// Process sent UTXOs for address cohorts: age metrics, cohort membership,
|
||||
/// and empty-address transitions.
|
||||
///
|
||||
/// For each spent UTXO:
|
||||
/// 1. Look up address data
|
||||
/// 2. Calculate age metrics
|
||||
/// 3. Update address balance and cohort membership
|
||||
/// 4. Handle addresses becoming empty
|
||||
///
|
||||
/// Note: Takes separate price/timestamp slices instead of chain_state to allow
|
||||
/// parallel execution with UTXO cohort processing (which mutates chain_state).
|
||||
///
|
||||
/// `price_range_max` is used to compute the peak price during each UTXO's holding period
|
||||
/// for accurate peak regret calculation.
|
||||
/// Takes separate price/timestamp slices rather than `chain_state` so it can
|
||||
/// run in parallel with UTXO cohort processing (which mutates `chain_state`).
|
||||
/// `price_range_max` feeds peak-regret computation via max price during
|
||||
/// each UTXO's holding period.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn process_sent(
|
||||
sent_data: HeightToAddrTypeToVec<(TypeIndex, Sats)>,
|
||||
@@ -35,15 +26,7 @@ pub(crate) fn process_sent(
|
||||
lookup: &mut AddrLookup<'_>,
|
||||
current_price: Cents,
|
||||
price_range_max: &PriceRangeMax,
|
||||
addr_count: &mut ByAddrType<u64>,
|
||||
empty_addr_count: &mut ByAddrType<u64>,
|
||||
activity_counts: &mut AddrTypeToActivityCounts,
|
||||
reused_addr_count: &mut AddrTypeToReusedAddrCount,
|
||||
input_from_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
active_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
total_exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
exposed_supply: &mut AddrTypeToExposedSupply,
|
||||
state: &mut AddrMetricsState,
|
||||
received_addrs: &ByAddrType<FxHashSet<TypeIndex>>,
|
||||
height_to_price: &[Cents],
|
||||
height_to_timestamp: &[Timestamp],
|
||||
@@ -57,68 +40,22 @@ pub(crate) fn process_sent(
|
||||
let prev_price = height_to_price[receive_height.to_usize()];
|
||||
let prev_timestamp = height_to_timestamp[receive_height.to_usize()];
|
||||
let age = Age::new(current_timestamp, prev_timestamp);
|
||||
|
||||
// Compute peak spot price during holding period for peak regret
|
||||
let peak_price = price_range_max.max_between(receive_height, current_height);
|
||||
|
||||
for (output_type, vec) in by_type.unwrap().into_iter() {
|
||||
// Cache mutable refs for this address type
|
||||
let type_addr_count = addr_count.get_mut(output_type).unwrap();
|
||||
let type_empty_count = empty_addr_count.get_mut(output_type).unwrap();
|
||||
let type_activity = activity_counts.get_mut_unwrap(output_type);
|
||||
let type_reused_count = reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_input_from_reused_count =
|
||||
input_from_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_active_reused_count = active_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_count = exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_total_exposed_count = total_exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_supply = exposed_supply.get_mut(output_type).unwrap();
|
||||
let type_received = received_addrs.get(output_type);
|
||||
let type_seen = seen_senders.get_mut_unwrap(output_type);
|
||||
|
||||
for (type_index, value) in vec {
|
||||
let addr_data = lookup.get_for_send(output_type, type_index);
|
||||
|
||||
// "Input from a reused address" event: the sending
|
||||
// address is in the reused set (lifetime
|
||||
// funded_txo_count > 1). Checked once per input. The
|
||||
// spend itself doesn't touch funded_txo_count so the
|
||||
// predicate is stable before/after `cohort_state.send`.
|
||||
if addr_data.is_reused() {
|
||||
*type_input_from_reused_count += 1;
|
||||
}
|
||||
let pre = AddrSendPreState::capture(addr_data, output_type);
|
||||
|
||||
let prev_balance = addr_data.balance();
|
||||
let new_balance = prev_balance.checked_sub(value).unwrap();
|
||||
|
||||
// On first encounter of this address this block, track activity
|
||||
if type_seen.insert(type_index) {
|
||||
type_activity.sending += 1;
|
||||
|
||||
let also_received = type_received.is_some_and(|s| s.contains(&type_index));
|
||||
// Track "bidirectional": addresses that sent AND
|
||||
// received this block.
|
||||
if also_received {
|
||||
type_activity.bidirectional += 1;
|
||||
}
|
||||
|
||||
// Block-level "active reused address" count: count
|
||||
// every distinct sender that's reused, but skip
|
||||
// those that also received this block (already
|
||||
// counted in process_received).
|
||||
if !also_received && addr_data.is_reused() {
|
||||
*type_active_reused_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let is_first_encounter = type_seen.insert(type_index);
|
||||
let also_received = type_received.is_some_and(|s| s.contains(&type_index));
|
||||
let will_be_empty = addr_data.has_1_utxos();
|
||||
|
||||
// Capture exposed state BEFORE the spend mutates spent_txo_count.
|
||||
let was_pubkey_exposed = addr_data.is_pubkey_exposed(output_type);
|
||||
let exposed_contribution_before =
|
||||
addr_data.exposed_supply_contribution(output_type);
|
||||
|
||||
// Compute buckets once
|
||||
let prev_bucket = AmountBucket::from(prev_balance);
|
||||
let new_bucket = AmountBucket::from(new_balance);
|
||||
let crossing_boundary = prev_bucket != new_bucket;
|
||||
@@ -130,50 +67,21 @@ pub(crate) fn process_sent(
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
|
||||
// Mutates addr_data.spent_txo_count (+= 1). on_send_applied reads the post-spend view.
|
||||
cohort_state.send(addr_data, value, current_price, prev_price, peak_price, age)?;
|
||||
// addr_data.spent_txo_count is now incremented by 1.
|
||||
state.on_send_applied(
|
||||
output_type,
|
||||
addr_data,
|
||||
&pre,
|
||||
is_first_encounter,
|
||||
also_received,
|
||||
will_be_empty,
|
||||
);
|
||||
|
||||
// Update exposed supply via post-spend contribution delta.
|
||||
let exposed_contribution_after = addr_data.exposed_supply_contribution(output_type);
|
||||
if exposed_contribution_after >= exposed_contribution_before {
|
||||
*type_exposed_supply +=
|
||||
exposed_contribution_after - exposed_contribution_before;
|
||||
} else {
|
||||
*type_exposed_supply -=
|
||||
exposed_contribution_before - exposed_contribution_after;
|
||||
}
|
||||
|
||||
// Update exposed counts on first-ever pubkey exposure.
|
||||
// For non-pk-exposed types this fires on the first spend; for
|
||||
// pk-exposed types it never fires here (was_pubkey_exposed was
|
||||
// already true at first receive in process_received).
|
||||
if !was_pubkey_exposed {
|
||||
*type_total_exposed_count += 1;
|
||||
if !will_be_empty {
|
||||
*type_exposed_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If crossing a bucket boundary, remove the (now-updated) address from old bucket
|
||||
if will_be_empty || crossing_boundary {
|
||||
cohort_state.subtract(addr_data);
|
||||
}
|
||||
|
||||
// Migrate address to new bucket or mark as empty
|
||||
if will_be_empty {
|
||||
*type_addr_count -= 1;
|
||||
*type_empty_count += 1;
|
||||
// Reused addr leaving the funded reused set
|
||||
if addr_data.is_reused() {
|
||||
*type_reused_count -= 1;
|
||||
}
|
||||
// Exposed addr leaving the funded exposed set: was in set
|
||||
// iff its pubkey was exposed pre-spend (since it was funded
|
||||
// to be in process_sent in the first place), and now leaves
|
||||
// because it's empty.
|
||||
if was_pubkey_exposed {
|
||||
*type_exposed_count -= 1;
|
||||
}
|
||||
lookup.move_to_empty(output_type, type_index);
|
||||
} else if crossing_boundary {
|
||||
cohorts
|
||||
|
||||
@@ -11,10 +11,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec, unli
|
||||
|
||||
use crate::{
|
||||
distribution::{
|
||||
addr::{
|
||||
AddrTypeToActivityCounts, AddrTypeToAddrCount, AddrTypeToExposedAddrCount,
|
||||
AddrTypeToExposedSupply, AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount,
|
||||
},
|
||||
addr::AddrMetricsState,
|
||||
block::{
|
||||
AddrCache, InputsResult, process_inputs, process_outputs, process_received,
|
||||
process_sent,
|
||||
@@ -193,51 +190,9 @@ pub(crate) fn process_blocks(
|
||||
.first_index
|
||||
.collect_range_at(start_usize, end_usize);
|
||||
|
||||
// Track running totals - recover from previous height if resuming
|
||||
debug!("recovering addr_counts from height {}", starting_height);
|
||||
let (
|
||||
mut addr_counts,
|
||||
mut empty_addr_counts,
|
||||
mut reused_addr_counts,
|
||||
mut total_reused_addr_counts,
|
||||
mut exposed_addr_counts,
|
||||
mut total_exposed_addr_counts,
|
||||
mut exposed_supply,
|
||||
) = if starting_height > Height::ZERO {
|
||||
(
|
||||
AddrTypeToAddrCount::from((&vecs.addrs.funded.by_addr_type, starting_height)),
|
||||
AddrTypeToAddrCount::from((&vecs.addrs.empty.by_addr_type, starting_height)),
|
||||
AddrTypeToReusedAddrCount::from((&vecs.addrs.reused.count.funded, starting_height)),
|
||||
AddrTypeToReusedAddrCount::from((&vecs.addrs.reused.count.total, starting_height)),
|
||||
AddrTypeToExposedAddrCount::from((&vecs.addrs.exposed.count.funded, starting_height)),
|
||||
AddrTypeToExposedAddrCount::from((&vecs.addrs.exposed.count.total, starting_height)),
|
||||
AddrTypeToExposedSupply::from((&vecs.addrs.exposed.supply, starting_height)),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
AddrTypeToAddrCount::default(),
|
||||
AddrTypeToAddrCount::default(),
|
||||
AddrTypeToReusedAddrCount::default(),
|
||||
AddrTypeToReusedAddrCount::default(),
|
||||
AddrTypeToExposedAddrCount::default(),
|
||||
AddrTypeToExposedAddrCount::default(),
|
||||
AddrTypeToExposedSupply::default(),
|
||||
)
|
||||
};
|
||||
debug!("addr_counts recovered");
|
||||
|
||||
// Track activity counts - reset each block
|
||||
let mut activity_counts = AddrTypeToActivityCounts::default();
|
||||
// Reused-addr event counts (receive + spend side). Per-block
|
||||
// flow, reset each block.
|
||||
let mut output_to_reused_addr_counts = AddrTypeToReusedAddrEventCount::default();
|
||||
let mut input_from_reused_addr_counts = AddrTypeToReusedAddrEventCount::default();
|
||||
// Distinct addresses active this block whose lifetime
|
||||
// funded_txo_count > 1 after this block's events. Incremented in
|
||||
// process_received for every receiver that ends up reused, and in
|
||||
// process_sent for every sender that's reused AND didn't also
|
||||
// receive this block (deduped via `received_addrs`).
|
||||
let mut active_reused_addr_counts = AddrTypeToReusedAddrEventCount::default();
|
||||
debug!("recovering addr metrics state from height {}", starting_height);
|
||||
let mut state = AddrMetricsState::from((&vecs.addrs, starting_height));
|
||||
debug!("addr metrics state recovered");
|
||||
|
||||
debug!("creating AddrCache");
|
||||
let mut cache = AddrCache::new();
|
||||
@@ -253,12 +208,7 @@ pub(crate) fn process_blocks(
|
||||
vecs.utxo_cohorts
|
||||
.par_iter_vecs_mut()
|
||||
.chain(vecs.addr_cohorts.par_iter_vecs_mut())
|
||||
.chain(vecs.addrs.funded.par_iter_height_mut())
|
||||
.chain(vecs.addrs.empty.par_iter_height_mut())
|
||||
.chain(vecs.addrs.activity.par_iter_height_mut())
|
||||
.chain(vecs.addrs.reused.par_iter_height_mut())
|
||||
.chain(vecs.addrs.exposed.par_iter_height_mut())
|
||||
.chain(vecs.addrs.avg_amount.par_iter_height_mut())
|
||||
.chain(vecs.addrs.par_iter_height_mut())
|
||||
.chain(rayon::iter::once(
|
||||
&mut vecs.coinblocks_destroyed.block as &mut dyn AnyStoredVec,
|
||||
))
|
||||
@@ -309,11 +259,7 @@ pub(crate) fn process_blocks(
|
||||
p2wsh: TypeIndex::from(first_p2wsh_vec[offset].to_usize()),
|
||||
};
|
||||
|
||||
// Reset per-block activity counts
|
||||
activity_counts.reset();
|
||||
output_to_reused_addr_counts.reset();
|
||||
input_from_reused_addr_counts.reset();
|
||||
active_reused_addr_counts.reset();
|
||||
state.reset_per_block();
|
||||
|
||||
// Process outputs, inputs, and tick-tock in parallel via rayon::join.
|
||||
// Collection (build tx_index mappings + bulk mmap reads) is merged into the
|
||||
@@ -474,40 +420,21 @@ pub(crate) fn process_blocks(
|
||||
|| -> Result<()> {
|
||||
let mut lookup = cache.as_lookup();
|
||||
|
||||
// Process received outputs (addresses receiving funds)
|
||||
process_received(
|
||||
outputs_result.received_data,
|
||||
&mut vecs.addr_cohorts,
|
||||
&mut lookup,
|
||||
block_price,
|
||||
&mut addr_counts,
|
||||
&mut empty_addr_counts,
|
||||
&mut activity_counts,
|
||||
&mut reused_addr_counts,
|
||||
&mut total_reused_addr_counts,
|
||||
&mut output_to_reused_addr_counts,
|
||||
&mut active_reused_addr_counts,
|
||||
&mut exposed_addr_counts,
|
||||
&mut total_exposed_addr_counts,
|
||||
&mut exposed_supply,
|
||||
&mut state,
|
||||
);
|
||||
|
||||
// Process sent inputs (addresses sending funds)
|
||||
process_sent(
|
||||
inputs_result.sent_data,
|
||||
&mut vecs.addr_cohorts,
|
||||
&mut lookup,
|
||||
block_price,
|
||||
ctx.price_range_max,
|
||||
&mut addr_counts,
|
||||
&mut empty_addr_counts,
|
||||
&mut activity_counts,
|
||||
&mut reused_addr_counts,
|
||||
&mut input_from_reused_addr_counts,
|
||||
&mut active_reused_addr_counts,
|
||||
&mut exposed_addr_counts,
|
||||
&mut total_exposed_addr_counts,
|
||||
&mut exposed_supply,
|
||||
&mut state,
|
||||
&received_addrs,
|
||||
height_to_price_vec,
|
||||
height_to_timestamp_vec,
|
||||
@@ -522,44 +449,10 @@ pub(crate) fn process_blocks(
|
||||
// Update Fenwick tree from pending deltas (must happen before push_cohort_states drains pending)
|
||||
vecs.utxo_cohorts.update_fenwick_from_pending();
|
||||
|
||||
// Push to height-indexed vectors
|
||||
vecs.addrs
|
||||
.funded
|
||||
.push_height(addr_counts.sum(), &addr_counts);
|
||||
vecs.addrs
|
||||
.empty
|
||||
.push_height(empty_addr_counts.sum(), &empty_addr_counts);
|
||||
vecs.addrs.activity.push_height(&activity_counts);
|
||||
vecs.addrs.reused.count.funded.push_height(
|
||||
reused_addr_counts.sum(),
|
||||
reused_addr_counts.values().copied(),
|
||||
);
|
||||
vecs.addrs.reused.count.total.push_height(
|
||||
total_reused_addr_counts.sum(),
|
||||
total_reused_addr_counts.values().copied(),
|
||||
);
|
||||
let activity_totals = activity_counts.totals();
|
||||
let activity_totals = state.activity.totals();
|
||||
let active_addr_count =
|
||||
activity_totals.sending + activity_totals.receiving - activity_totals.bidirectional;
|
||||
let active_reused = u32::try_from(active_reused_addr_counts.sum()).unwrap();
|
||||
vecs.addrs.reused.events.push_height(
|
||||
&output_to_reused_addr_counts,
|
||||
&input_from_reused_addr_counts,
|
||||
active_addr_count,
|
||||
active_reused,
|
||||
);
|
||||
vecs.addrs.exposed.count.funded.push_height(
|
||||
exposed_addr_counts.sum(),
|
||||
exposed_addr_counts.values().copied(),
|
||||
);
|
||||
vecs.addrs.exposed.count.total.push_height(
|
||||
total_exposed_addr_counts.sum(),
|
||||
total_exposed_addr_counts.values().copied(),
|
||||
);
|
||||
vecs.addrs
|
||||
.exposed
|
||||
.supply
|
||||
.push_height(exposed_supply.sum(), exposed_supply.values().copied());
|
||||
vecs.addrs.push_height(&state, active_addr_count);
|
||||
|
||||
let is_last_of_day = is_last_of_day[offset];
|
||||
let date_opt = is_last_of_day.then(|| Date::from(timestamp));
|
||||
@@ -632,7 +525,7 @@ fn push_cohort_states(
|
||||
height: Height,
|
||||
height_price: Cents,
|
||||
) {
|
||||
// Phase 1: push + unrealized (no reset yet; states still needed for aggregation)
|
||||
// Phase 1: push + unrealized (no reset yet, states still needed for aggregation)
|
||||
rayon::join(
|
||||
|| {
|
||||
utxo_cohorts.par_iter_separate_mut().for_each(|v| {
|
||||
|
||||
@@ -76,11 +76,7 @@ pub(crate) fn write(
|
||||
vecs.any_addr_indexes
|
||||
.par_iter_mut()
|
||||
.chain(vecs.addrs_data.par_iter_mut())
|
||||
.chain(vecs.addrs.funded.par_iter_height_mut())
|
||||
.chain(vecs.addrs.empty.par_iter_height_mut())
|
||||
.chain(vecs.addrs.activity.par_iter_height_mut())
|
||||
.chain(vecs.addrs.reused.par_iter_height_mut())
|
||||
.chain(vecs.addrs.exposed.par_iter_height_mut())
|
||||
.chain(vecs.addrs.par_iter_stateful_height_mut())
|
||||
.chain(
|
||||
[
|
||||
&mut vecs.supply_state as &mut dyn AnyStoredVec,
|
||||
|
||||
@@ -8,9 +8,10 @@ use brk_types::{
|
||||
Cents, EmptyAddrData, EmptyAddrIndex, FundedAddrData, FundedAddrIndex, Height, Indexes,
|
||||
StoredF64, SupplyState, Timestamp, TxIndex, Version,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use tracing::{debug, info};
|
||||
use vecdb::{
|
||||
AnyVec, BytesVec, Database, Exit, ImportableVec, LazyVecFrom1, ReadOnlyClone,
|
||||
AnyStoredVec, AnyVec, BytesVec, Database, Exit, ImportableVec, LazyVecFrom1, ReadOnlyClone,
|
||||
ReadableCloneableVec, ReadableVec, Rw, Stamp, StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
@@ -34,8 +35,8 @@ use crate::{
|
||||
use super::{
|
||||
AddrCohorts, AddrsDataVecs, AnyAddrIndexesVecs, RangeMap, UTXOCohorts,
|
||||
addr::{
|
||||
AddrActivityVecs, AddrCountsVecs, DeltaVecs, ExposedAddrVecs, NewAddrCountVecs,
|
||||
ReusedAddrVecs, TotalAddrCountVecs,
|
||||
AddrActivityVecs, AddrCountsVecs, AddrMetricsState, DeltaVecs, ExposedAddrVecs,
|
||||
NewAddrCountVecs, ReusedAddrVecs, TotalAddrCountVecs,
|
||||
},
|
||||
metrics::AvgAmountMetrics,
|
||||
};
|
||||
@@ -50,6 +51,7 @@ pub struct AddrMetricsVecs<M: StorageMode = Rw> {
|
||||
pub total: TotalAddrCountVecs<M>,
|
||||
pub new: NewAddrCountVecs<M>,
|
||||
pub reused: ReusedAddrVecs<M>,
|
||||
pub respent: ReusedAddrVecs<M>,
|
||||
pub exposed: ExposedAddrVecs<M>,
|
||||
pub delta: DeltaVecs,
|
||||
pub avg_amount: WithAddrTypes<AvgAmountMetrics<M>>,
|
||||
@@ -60,6 +62,71 @@ pub struct AddrMetricsVecs<M: StorageMode = Rw> {
|
||||
pub empty_index: LazyVecFrom1<EmptyAddrIndex, EmptyAddrIndex, EmptyAddrIndex, EmptyAddrData>,
|
||||
}
|
||||
|
||||
impl AddrMetricsVecs {
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.funded.reset_height()?;
|
||||
self.empty.reset_height()?;
|
||||
self.activity.reset_height()?;
|
||||
self.reused.reset_height()?;
|
||||
self.respent.reset_height()?;
|
||||
self.exposed.reset_height()?;
|
||||
self.avg_amount.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.funded
|
||||
.min_stateful_len()
|
||||
.min(self.empty.min_stateful_len())
|
||||
.min(self.activity.min_stateful_len())
|
||||
.min(self.reused.min_stateful_len())
|
||||
.min(self.respent.min_stateful_len())
|
||||
.min(self.exposed.min_stateful_len())
|
||||
}
|
||||
|
||||
/// Stateful vecs pushed per block. Mirrors [`Self::push_height`] and
|
||||
/// [`Self::min_stateful_len`]. Used by the stamped write path.
|
||||
pub(crate) fn par_iter_stateful_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.funded
|
||||
.par_iter_height_mut()
|
||||
.chain(self.empty.par_iter_height_mut())
|
||||
.chain(self.activity.par_iter_height_mut())
|
||||
.chain(self.reused.par_iter_height_mut())
|
||||
.chain(self.respent.par_iter_height_mut())
|
||||
.chain(self.exposed.par_iter_height_mut())
|
||||
}
|
||||
|
||||
/// All height-indexed vecs including derived (`avg_amount`). Used for
|
||||
/// bulk truncation, where derived vecs must follow the stateful ones.
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.funded
|
||||
.par_iter_height_mut()
|
||||
.chain(self.empty.par_iter_height_mut())
|
||||
.chain(self.activity.par_iter_height_mut())
|
||||
.chain(self.reused.par_iter_height_mut())
|
||||
.chain(self.respent.par_iter_height_mut())
|
||||
.chain(self.exposed.par_iter_height_mut())
|
||||
.chain(self.avg_amount.par_iter_height_mut())
|
||||
}
|
||||
|
||||
/// Push one block's worth of per-addr-type running totals to all
|
||||
/// height-indexed vecs. `active_addr_count` is the block-level total
|
||||
/// of active addresses (sending + receiving - bidirectional).
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, state: &AddrMetricsState, active_addr_count: u32) {
|
||||
self.funded.push_counts(&state.funded);
|
||||
self.empty.push_counts(&state.empty);
|
||||
self.activity.push_height(&state.activity);
|
||||
self.exposed.push_height(&state.exposed);
|
||||
self.reused.push_height(&state.reused, active_addr_count);
|
||||
self.respent.push_height(&state.respent, active_addr_count);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
#[traversable(skip)]
|
||||
@@ -162,9 +229,14 @@ impl Vecs {
|
||||
// Per-block delta of total (global + per-type)
|
||||
let new_addr_count = NewAddrCountVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
|
||||
// Reused address tracking (counts + per-block uses + percent)
|
||||
// Reused address tracking (counts + per-block uses + percent).
|
||||
// `reused_*` uses the receive-side predicate (funded_txo_count > 1,
|
||||
// industry standard). `respent_*` uses the spend-side counterpart
|
||||
// (spent_txo_count > 1, strictly more restrictive).
|
||||
let reused_addr_count =
|
||||
ReusedAddrVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
ReusedAddrVecs::forced_import(&db, "reused", version, indexes, cached_starts)?;
|
||||
let respent_addr_count =
|
||||
ReusedAddrVecs::forced_import(&db, "respent", version, indexes, cached_starts)?;
|
||||
|
||||
// Exposed address tracking (counts + supply) - quantum / pubkey-exposure sense
|
||||
let exposed_addr_vecs = ExposedAddrVecs::forced_import(&db, version, indexes)?;
|
||||
@@ -188,6 +260,7 @@ impl Vecs {
|
||||
total: total_addr_count,
|
||||
new: new_addr_count,
|
||||
reused: reused_addr_count,
|
||||
respent: respent_addr_count,
|
||||
exposed: exposed_addr_vecs,
|
||||
delta,
|
||||
avg_amount,
|
||||
@@ -303,12 +376,7 @@ impl Vecs {
|
||||
|
||||
if needs_fresh_start {
|
||||
self.supply_state.reset()?;
|
||||
self.addrs.funded.reset_height()?;
|
||||
self.addrs.empty.reset_height()?;
|
||||
self.addrs.activity.reset_height()?;
|
||||
self.addrs.reused.reset_height()?;
|
||||
self.addrs.exposed.reset_height()?;
|
||||
self.addrs.avg_amount.reset_height()?;
|
||||
self.addrs.reset_height()?;
|
||||
reset_state(
|
||||
&mut self.any_addr_indexes,
|
||||
&mut self.addrs_data,
|
||||
@@ -478,21 +546,34 @@ impl Vecs {
|
||||
// 6b. Compute address count sum (by addr_type -> all)
|
||||
self.addrs.funded.compute_rest(starting_indexes, exit)?;
|
||||
self.addrs.empty.compute_rest(starting_indexes, exit)?;
|
||||
self.addrs.reused.compute_rest(
|
||||
starting_indexes,
|
||||
&outputs.by_type,
|
||||
&inputs.by_type,
|
||||
exit,
|
||||
)?;
|
||||
let t = &self.utxo_cohorts.type_;
|
||||
let type_supply_sats = ByAddrType::new(|filter| {
|
||||
let Filter::Type(ot) = filter else { unreachable!() };
|
||||
&t.get(ot).metrics.supply.total.sats.height
|
||||
});
|
||||
let all_supply_sats = &self.utxo_cohorts.all.metrics.supply.total.sats.height;
|
||||
self.addrs.reused.compute_rest(
|
||||
starting_indexes,
|
||||
&outputs.by_type,
|
||||
&inputs.by_type,
|
||||
prices,
|
||||
all_supply_sats,
|
||||
&type_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
self.addrs.respent.compute_rest(
|
||||
starting_indexes,
|
||||
&outputs.by_type,
|
||||
&inputs.by_type,
|
||||
prices,
|
||||
all_supply_sats,
|
||||
&type_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
self.addrs.exposed.compute_rest(
|
||||
starting_indexes,
|
||||
prices,
|
||||
&self.utxo_cohorts.all.metrics.supply.total.sats.height,
|
||||
all_supply_sats,
|
||||
&type_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
@@ -605,11 +686,7 @@ impl Vecs {
|
||||
.min(Height::from(self.supply_state.len()))
|
||||
.min(self.any_addr_indexes.min_stamped_len())
|
||||
.min(self.addrs_data.min_stamped_len())
|
||||
.min(Height::from(self.addrs.funded.min_stateful_len()))
|
||||
.min(Height::from(self.addrs.empty.min_stateful_len()))
|
||||
.min(Height::from(self.addrs.activity.min_stateful_len()))
|
||||
.min(Height::from(self.addrs.reused.min_stateful_len()))
|
||||
.min(Height::from(self.addrs.exposed.min_stateful_len()))
|
||||
.min(Height::from(self.addrs.min_stateful_len()))
|
||||
.min(Height::from(self.coinblocks_destroyed.block.len()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,10 +266,14 @@ impl Computer {
|
||||
}
|
||||
|
||||
if let Some(name) = entry.file_name().to_str()
|
||||
&& !name.starts_with('_')
|
||||
&& !EXPECTED_DBS.contains(&name)
|
||||
{
|
||||
info!("Removing obsolete database folder: {}", name);
|
||||
fs::remove_dir_all(entry.path())?;
|
||||
let path = entry.path();
|
||||
fs::remove_dir_all(&path).map_err(|e| {
|
||||
std::io::Error::other(format!("remove_dir_all {path:?}: {e}"))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,13 +45,14 @@ pub fn linearize_clusters(graph: &Graph) -> Vec<Package> {
|
||||
let clusters = find_components(graph);
|
||||
let mut packages: Vec<Package> = Vec::with_capacity(clusters.len());
|
||||
|
||||
for cluster in clusters {
|
||||
for (cluster_id, cluster) in clusters.into_iter().enumerate() {
|
||||
let cluster_id = cluster_id as u32;
|
||||
if cluster.nodes.len() == 1 {
|
||||
packages.push(singleton_package(&cluster));
|
||||
packages.push(singleton_package(&cluster, cluster_id));
|
||||
continue;
|
||||
}
|
||||
for chunk in sfl::linearize(&cluster) {
|
||||
packages.push(chunk_to_package(&cluster, &chunk));
|
||||
for (chunk_order, chunk) in sfl::linearize(&cluster).iter().enumerate() {
|
||||
packages.push(chunk_to_package(&cluster, chunk, cluster_id, chunk_order as u32));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,19 +169,24 @@ fn kahn_topo_rank(nodes: &[ClusterNode]) -> Vec<u32> {
|
||||
}
|
||||
|
||||
/// Build a one-tx `Package` for a cluster of size 1.
|
||||
fn singleton_package(cluster: &Cluster) -> Package {
|
||||
fn singleton_package(cluster: &Cluster, cluster_id: u32) -> Package {
|
||||
let node = &cluster.nodes[0];
|
||||
let fee_rate = FeeRate::from((node.fee, node.vsize));
|
||||
let mut package = Package::new(fee_rate);
|
||||
let mut package = Package::new(fee_rate, cluster_id, 0);
|
||||
package.add_tx(node.tx_index, u64::from(node.vsize));
|
||||
package
|
||||
}
|
||||
|
||||
/// Convert an SFL-emitted chunk (set of local indices) into a `Package`.
|
||||
/// Txs inside the package are ordered parents-first by `topo_rank`.
|
||||
fn chunk_to_package(cluster: &Cluster, chunk: &sfl::Chunk) -> Package {
|
||||
fn chunk_to_package(
|
||||
cluster: &Cluster,
|
||||
chunk: &sfl::Chunk,
|
||||
cluster_id: u32,
|
||||
chunk_order: u32,
|
||||
) -> Package {
|
||||
let fee_rate = FeeRate::from((Sats::from(chunk.fee), VSize::from(chunk.vsize)));
|
||||
let mut package = Package::new(fee_rate);
|
||||
let mut package = Package::new(fee_rate, cluster_id, chunk_order);
|
||||
|
||||
let mut ordered: SmallVec<[LocalIdx; 8]> = chunk.nodes.iter().copied().collect();
|
||||
ordered.sort_by_key(|&local| cluster.topo_rank[local as usize]);
|
||||
|
||||
@@ -35,7 +35,11 @@ pub fn linearize(cluster: &Cluster) -> Vec<Chunk> {
|
||||
if n == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
assert!(n <= BITMASK_LIMIT, "cluster size {} exceeds u128 capacity", n);
|
||||
assert!(
|
||||
n <= BITMASK_LIMIT,
|
||||
"cluster size {} exceeds u128 capacity",
|
||||
n
|
||||
);
|
||||
|
||||
let mut parents_mask: Vec<u128> = vec![0; n];
|
||||
let mut ancestor_incl: Vec<u128> = vec![0; n];
|
||||
@@ -97,6 +101,7 @@ fn best_subset(
|
||||
best
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn recurse(
|
||||
idx: usize,
|
||||
topo_order: &[LocalIdx],
|
||||
@@ -120,18 +125,34 @@ fn recurse(
|
||||
|
||||
// Not in remaining, or a parent (within remaining) is excluded:
|
||||
// this node is forced-excluded, no branching.
|
||||
if (bit & remaining) == 0
|
||||
|| (parents_mask[node as usize] & remaining & !included) != 0
|
||||
{
|
||||
if (bit & remaining) == 0 || (parents_mask[node as usize] & remaining & !included) != 0 {
|
||||
recurse(
|
||||
idx + 1, topo_order, parents_mask, remaining, included, f, v, fee_of, vsize_of, best,
|
||||
idx + 1,
|
||||
topo_order,
|
||||
parents_mask,
|
||||
remaining,
|
||||
included,
|
||||
f,
|
||||
v,
|
||||
fee_of,
|
||||
vsize_of,
|
||||
best,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Exclude
|
||||
recurse(
|
||||
idx + 1, topo_order, parents_mask, remaining, included, f, v, fee_of, vsize_of, best,
|
||||
idx + 1,
|
||||
topo_order,
|
||||
parents_mask,
|
||||
remaining,
|
||||
included,
|
||||
f,
|
||||
v,
|
||||
fee_of,
|
||||
vsize_of,
|
||||
best,
|
||||
);
|
||||
// Include
|
||||
recurse(
|
||||
|
||||
@@ -9,7 +9,7 @@ pub use package::Package;
|
||||
use crate::entry::Entry;
|
||||
|
||||
/// Target vsize per block (~1MB, derived from 4MW weight limit).
|
||||
const BLOCK_VSIZE: u64 = 1_000_000;
|
||||
pub(crate) const BLOCK_VSIZE: u64 = 1_000_000;
|
||||
|
||||
/// Number of projected blocks to build (last one is a catch-all overflow).
|
||||
const NUM_BLOCKS: usize = 8;
|
||||
|
||||
@@ -9,19 +9,27 @@ use crate::types::TxIndex;
|
||||
/// i.e. what a miner collects per vsize when the package is mined.
|
||||
/// Packages are produced by SFL in descending-`fee_rate` order within a
|
||||
/// cluster and are atomic (all-or-nothing) at mining time.
|
||||
///
|
||||
/// `cluster_id` + `chunk_order` let the partitioner enforce intra-cluster
|
||||
/// ordering when its look-ahead would otherwise pull a child chunk into
|
||||
/// an earlier block than its parent chunk.
|
||||
pub struct Package {
|
||||
/// Transactions in topological order (parents before children).
|
||||
pub txs: Vec<TxIndex>,
|
||||
pub vsize: u64,
|
||||
pub fee_rate: FeeRate,
|
||||
pub cluster_id: u32,
|
||||
pub chunk_order: u32,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
pub fn new(fee_rate: FeeRate) -> Self {
|
||||
pub fn new(fee_rate: FeeRate, cluster_id: u32, chunk_order: u32) -> Self {
|
||||
Self {
|
||||
txs: Vec::new(),
|
||||
vsize: 0,
|
||||
fee_rate,
|
||||
cluster_id,
|
||||
chunk_order,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,21 +11,30 @@ const LOOK_AHEAD_COUNT: usize = 100;
|
||||
/// chunks. The final block is a catch-all containing every remaining
|
||||
/// package, so no low-rate tx is silently dropped from the projection
|
||||
/// (matches mempool.space's last-block behavior).
|
||||
///
|
||||
/// Look-ahead respects intra-cluster order: a chunk is only taken once
|
||||
/// every earlier-rate chunk of the same cluster has been placed, so a
|
||||
/// child chunk never lands in an earlier block than its parent chunk.
|
||||
pub fn partition_into_blocks(
|
||||
mut packages: Vec<Package>,
|
||||
num_blocks: usize,
|
||||
) -> Vec<Vec<Package>> {
|
||||
// Stable sort for deterministic output across equal fee rates. SFL
|
||||
// guarantees chunks within a cluster come in non-increasing rate
|
||||
// order, so stable sorting by fee_rate preserves intra-cluster
|
||||
// topology automatically.
|
||||
// Stable sort preserves SFL's per-cluster non-increasing-rate emission
|
||||
// order in the global list, which is what `cluster_next` relies on.
|
||||
packages.sort_by_key(|p| Reverse(p.fee_rate));
|
||||
|
||||
let num_clusters = packages
|
||||
.iter()
|
||||
.map(|p| p.cluster_id as usize + 1)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let mut cluster_next: Vec<u32> = vec![0; num_clusters];
|
||||
|
||||
let mut slots: Vec<Option<Package>> = packages.into_iter().map(Some).collect();
|
||||
let mut blocks: Vec<Vec<Package>> = Vec::with_capacity(num_blocks);
|
||||
let normal_blocks = num_blocks.saturating_sub(1);
|
||||
|
||||
let mut idx = fill_normal_blocks(&mut slots, &mut blocks, normal_blocks);
|
||||
let mut idx = fill_normal_blocks(&mut slots, &mut blocks, normal_blocks, &mut cluster_next);
|
||||
|
||||
if blocks.len() < num_blocks {
|
||||
let mut overflow: Vec<Package> = Vec::new();
|
||||
@@ -49,6 +58,7 @@ fn fill_normal_blocks(
|
||||
slots: &mut [Option<Package>],
|
||||
blocks: &mut Vec<Vec<Package>>,
|
||||
target_blocks: usize,
|
||||
cluster_next: &mut [u32],
|
||||
) -> usize {
|
||||
let mut current_block: Vec<Package> = Vec::new();
|
||||
let mut current_vsize: u64 = 0;
|
||||
@@ -63,9 +73,7 @@ fn fill_normal_blocks(
|
||||
let remaining_space = BLOCK_VSIZE.saturating_sub(current_vsize);
|
||||
|
||||
if pkg.vsize <= remaining_space {
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
current_vsize += pkg.vsize;
|
||||
current_block.push(pkg);
|
||||
take(slots, idx, &mut current_block, &mut current_vsize, cluster_next);
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -73,9 +81,7 @@ fn fill_normal_blocks(
|
||||
if current_block.is_empty() {
|
||||
// Oversized package with no partial block to preserve; take it
|
||||
// anyway so we don't stall on a package larger than BLOCK_VSIZE.
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
current_vsize += pkg.vsize;
|
||||
current_block.push(pkg);
|
||||
take(slots, idx, &mut current_block, &mut current_vsize, cluster_next);
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -86,6 +92,7 @@ fn fill_normal_blocks(
|
||||
remaining_space,
|
||||
&mut current_block,
|
||||
&mut current_vsize,
|
||||
cluster_next,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -102,23 +109,44 @@ fn fill_normal_blocks(
|
||||
}
|
||||
|
||||
/// Scan the look-ahead window for a package small enough to fit in the
|
||||
/// remaining space and move it into the current block.
|
||||
/// remaining space, skipping any candidate whose cluster has an earlier
|
||||
/// unplaced chunk (that chunk's parents would land after its children).
|
||||
fn try_fill_with_smaller(
|
||||
slots: &mut [Option<Package>],
|
||||
start: usize,
|
||||
remaining_space: u64,
|
||||
block: &mut Vec<Package>,
|
||||
block_vsize: &mut u64,
|
||||
cluster_next: &mut [u32],
|
||||
) -> bool {
|
||||
let end = (start + LOOK_AHEAD_COUNT).min(slots.len());
|
||||
for idx in (start + 1)..end {
|
||||
let Some(pkg) = &slots[idx] else { continue };
|
||||
if pkg.vsize <= remaining_space {
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
*block_vsize += pkg.vsize;
|
||||
block.push(pkg);
|
||||
return true;
|
||||
if pkg.vsize > remaining_space {
|
||||
continue;
|
||||
}
|
||||
if pkg.chunk_order != cluster_next[pkg.cluster_id as usize] {
|
||||
continue;
|
||||
}
|
||||
take(slots, idx, block, block_vsize, cluster_next);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn take(
|
||||
slots: &mut [Option<Package>],
|
||||
idx: usize,
|
||||
block: &mut Vec<Package>,
|
||||
block_vsize: &mut u64,
|
||||
cluster_next: &mut [u32],
|
||||
) {
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
debug_assert_eq!(
|
||||
pkg.chunk_order, cluster_next[pkg.cluster_id as usize],
|
||||
"partitioner took a chunk out of cluster order"
|
||||
);
|
||||
cluster_next[pkg.cluster_id as usize] = pkg.chunk_order + 1;
|
||||
*block_vsize += pkg.vsize;
|
||||
block.push(pkg);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod fees;
|
||||
mod snapshot;
|
||||
mod stats;
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) mod verify;
|
||||
|
||||
pub use brk_types::RecommendedFees;
|
||||
pub use snapshot::Snapshot;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use brk_rpc::Client;
|
||||
use brk_types::{Sats, SatsSigned, TxidPrefix};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
block_builder::{BLOCK_VSIZE, Package},
|
||||
entry::Entry,
|
||||
types::TxIndex,
|
||||
};
|
||||
|
||||
type PrefixSet = FxHashSet<TxidPrefix>;
|
||||
type FeeByPrefix = FxHashMap<TxidPrefix, Sats>;
|
||||
|
||||
pub struct Verifier;
|
||||
|
||||
impl Verifier {
|
||||
pub fn check(client: &Client, blocks: &[Vec<Package>], entries: &[Option<Entry>]) {
|
||||
Self::check_structure(blocks, entries);
|
||||
Self::compare_to_core(client, blocks, entries);
|
||||
}
|
||||
|
||||
fn check_structure(blocks: &[Vec<Package>], entries: &[Option<Entry>]) {
|
||||
let in_pool: PrefixSet = entries
|
||||
.iter()
|
||||
.filter_map(|e| e.as_ref().map(Entry::txid_prefix))
|
||||
.collect();
|
||||
let mut placed = PrefixSet::default();
|
||||
|
||||
for (b, block) in blocks.iter().enumerate() {
|
||||
for (p, pkg) in block.iter().enumerate() {
|
||||
let mut summed_vsize = 0u64;
|
||||
for &tx_index in &pkg.txs {
|
||||
let entry = Self::live_entry(entries, tx_index, b, p);
|
||||
Self::assert_parents_placed_first(entry, &in_pool, &placed, b, p);
|
||||
Self::place(entry, &mut placed, b, p);
|
||||
summed_vsize += u64::from(entry.vsize);
|
||||
}
|
||||
assert_eq!(
|
||||
pkg.vsize, summed_vsize,
|
||||
"block {b} pkg {p}: pkg.vsize {} != sum {summed_vsize}",
|
||||
pkg.vsize
|
||||
);
|
||||
}
|
||||
if b + 1 < blocks.len() {
|
||||
Self::assert_block_fits_budget(block, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn live_entry<'e>(
|
||||
entries: &'e [Option<Entry>],
|
||||
tx_index: TxIndex,
|
||||
b: usize,
|
||||
p: usize,
|
||||
) -> &'e Entry {
|
||||
entries[tx_index.as_usize()]
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| panic!("block {b} pkg {p}: dead tx_index {tx_index:?}"))
|
||||
}
|
||||
|
||||
fn assert_parents_placed_first(
|
||||
entry: &Entry,
|
||||
in_pool: &PrefixSet,
|
||||
placed: &PrefixSet,
|
||||
b: usize,
|
||||
p: usize,
|
||||
) {
|
||||
for parent in &entry.depends {
|
||||
if in_pool.contains(parent) && !placed.contains(parent) {
|
||||
panic!(
|
||||
"block {b} pkg {p}: {} placed before its parent",
|
||||
entry.txid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn place(entry: &Entry, placed: &mut PrefixSet, b: usize, p: usize) {
|
||||
assert!(
|
||||
placed.insert(entry.txid_prefix()),
|
||||
"block {b} pkg {p}: duplicate txid {}",
|
||||
entry.txid
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_block_fits_budget(block: &[Package], b: usize) {
|
||||
let total: u64 = block.iter().map(|pkg| pkg.vsize).sum();
|
||||
let is_oversized_singleton = block.len() == 1 && total > BLOCK_VSIZE;
|
||||
if is_oversized_singleton {
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
total <= BLOCK_VSIZE,
|
||||
"block {b}: vsize {total} exceeds {BLOCK_VSIZE}"
|
||||
);
|
||||
}
|
||||
|
||||
fn compare_to_core(client: &Client, blocks: &[Vec<Package>], entries: &[Option<Entry>]) {
|
||||
let Some(next_block) = blocks.first() else {
|
||||
return;
|
||||
};
|
||||
let core: FeeByPrefix = match client.get_block_template_txs() {
|
||||
Ok(txs) => txs
|
||||
.into_iter()
|
||||
.map(|t| (TxidPrefix::from(&t.txid), t.fee))
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
warn!("verify: getblocktemplate failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let ours: FeeByPrefix = next_block
|
||||
.iter()
|
||||
.flat_map(|pkg| &pkg.txs)
|
||||
.filter_map(|&i| entries[i.as_usize()].as_ref())
|
||||
.map(|e| (e.txid_prefix(), e.fee))
|
||||
.collect();
|
||||
|
||||
let overlap = ours.keys().filter(|k| core.contains_key(k)).count();
|
||||
let union = ours.len() + core.len() - overlap;
|
||||
let jaccard = if union == 0 {
|
||||
1.0
|
||||
} else {
|
||||
overlap as f64 / union as f64
|
||||
};
|
||||
|
||||
let ours_fee: Sats = ours.values().copied().sum();
|
||||
let core_fee: Sats = core.values().copied().sum();
|
||||
let delta = SatsSigned::from(ours_fee) - SatsSigned::from(core_fee);
|
||||
let delta_bps = if core_fee == Sats::ZERO {
|
||||
0.0
|
||||
} else {
|
||||
f64::from(delta) / f64::from(core_fee) * 10_000.0
|
||||
};
|
||||
|
||||
debug!(
|
||||
"verify block 0: txs {}/{} (overlap {}, jaccard {:.3}) | fee {}/{} (delta {:+}, {:+.1} bps)",
|
||||
ours.len(),
|
||||
core.len(),
|
||||
overlap,
|
||||
jaccard,
|
||||
ours_fee,
|
||||
core_fee,
|
||||
delta.inner(),
|
||||
delta_bps,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -343,6 +343,10 @@ impl MempoolInner {
|
||||
let entries_slice = entries.entries();
|
||||
|
||||
let blocks = build_projected_blocks(entries_slice);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
crate::projected_blocks::verify::Verifier::check(&self.client, &blocks, entries_slice);
|
||||
|
||||
let snapshot = Snapshot::build(blocks, entries_slice);
|
||||
|
||||
*self.snapshot.write() = snapshot;
|
||||
|
||||
@@ -22,7 +22,8 @@ impl Query {
|
||||
})
|
||||
.collect();
|
||||
|
||||
cohorts.sort_by(|a, b| a.to_string().cmp(&b.to_string()));
|
||||
cohorts.sort_by_key(|a| a.to_string());
|
||||
|
||||
Ok(cohorts)
|
||||
}
|
||||
|
||||
@@ -76,12 +77,7 @@ impl Query {
|
||||
}
|
||||
|
||||
/// URPD for a cohort on a specific date.
|
||||
pub fn urpd_at(
|
||||
&self,
|
||||
cohort: &Cohort,
|
||||
date: Date,
|
||||
agg: UrpdAggregation,
|
||||
) -> Result<Urpd> {
|
||||
pub fn urpd_at(&self, cohort: &Cohort, date: Date, agg: UrpdAggregation) -> Result<Urpd> {
|
||||
let raw = self.urpd_raw(cohort, date)?;
|
||||
let day1 = Day1::try_from(date).map_err(|e| Error::Parse(e.to_string()))?;
|
||||
let close = self
|
||||
@@ -99,9 +95,9 @@ impl Query {
|
||||
/// URPD for the most recently available date in a cohort.
|
||||
pub fn urpd_latest(&self, cohort: &Cohort, agg: UrpdAggregation) -> Result<Urpd> {
|
||||
let dates = self.urpd_dates(cohort)?;
|
||||
let date = *dates.last().ok_or_else(|| {
|
||||
Error::NotFound(format!("No URPD available for cohort '{cohort}'"))
|
||||
})?;
|
||||
let date = *dates
|
||||
.last()
|
||||
.ok_or_else(|| Error::NotFound(format!("No URPD available for cohort '{cohort}'")))?;
|
||||
self.urpd_at(cohort, date, agg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ exclude = ["examples/"]
|
||||
|
||||
[features]
|
||||
default = ["corepc"]
|
||||
bitcoincore-rpc = ["dep:bitcoincore-rpc", "brk_error/bitcoincore-rpc"]
|
||||
bitcoincore-rpc = ["dep:bitcoincore-rpc", "dep:serde_json", "brk_error/bitcoincore-rpc"]
|
||||
corepc = ["dep:corepc-client", "dep:corepc-jsonrpc", "dep:serde_json", "dep:serde", "brk_error/corepc"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use bitcoincore_rpc::{Client as CoreClient, Error as RpcError, RpcApi, jsonrpc};
|
||||
use bitcoincore_rpc::{
|
||||
Client as CoreClient, Error as RpcError, RpcApi,
|
||||
json::{GetBlockTemplateCapabilities, GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
jsonrpc,
|
||||
};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::Sats;
|
||||
use brk_types::{Sats, Txid};
|
||||
use parking_lot::RwLock;
|
||||
use serde_json::value::RawValue;
|
||||
use tracing::info;
|
||||
|
||||
use super::{Auth, BlockHeaderInfo, BlockInfo, BlockchainInfo, RawMempoolEntry, TxOutInfo};
|
||||
use super::{
|
||||
Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, RawMempoolEntry, TxOutInfo,
|
||||
};
|
||||
|
||||
/// Per-batch request count for `get_block_hashes_range`. Sized so the
|
||||
/// JSON request body stays well under a megabyte and bitcoind doesn't
|
||||
@@ -310,4 +316,23 @@ impl ClientInner {
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<bitcoin::Txid> {
|
||||
Ok(self.call_once(|c| c.send_raw_transaction(hex))?)
|
||||
}
|
||||
|
||||
/// Transactions Bitcoin Core would include in the next block it would
|
||||
/// mine. Core requires the `segwit` rule to be declared.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
let r = self.call_with_retry(|c| {
|
||||
c.get_block_template(
|
||||
GetBlockTemplateModes::Template,
|
||||
&[GetBlockTemplateRules::SegWit],
|
||||
&[] as &[GetBlockTemplateCapabilities],
|
||||
)
|
||||
})?;
|
||||
Ok(r.transactions
|
||||
.into_iter()
|
||||
.map(|t| BlockTemplateTx {
|
||||
txid: Txid::from(t.txid),
|
||||
fee: Sats::from(t.fee.to_sat()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::Sats;
|
||||
use brk_types::{Sats, Txid};
|
||||
use corepc_client::client_sync::Auth as CorepcAuth;
|
||||
use parking_lot::RwLock;
|
||||
use serde_json::value::RawValue;
|
||||
use tracing::info;
|
||||
|
||||
use super::{Auth, BlockHeaderInfo, BlockInfo, BlockchainInfo, RawMempoolEntry, TxOutInfo};
|
||||
use super::{
|
||||
Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, RawMempoolEntry, TxOutInfo,
|
||||
};
|
||||
|
||||
type CoreClient = corepc_client::client_sync::v30::Client;
|
||||
type CoreError = corepc_client::client_sync::Error;
|
||||
@@ -186,11 +188,7 @@ impl ClientInner {
|
||||
/// a 50 MB request body or hold every response in memory at once.
|
||||
///
|
||||
/// Returns hashes in canonical order (`start`, `start+1`, …, `end`).
|
||||
pub fn get_block_hashes_range(
|
||||
&self,
|
||||
start: u64,
|
||||
end: u64,
|
||||
) -> Result<Vec<bitcoin::BlockHash>> {
|
||||
pub fn get_block_hashes_range(&self, start: u64, end: u64) -> Result<Vec<bitcoin::BlockHash>> {
|
||||
if end < start {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -370,6 +368,22 @@ impl ClientInner {
|
||||
c.call("sendrawtransaction", &args)
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Transactions Bitcoin Core would include in the next block it would
|
||||
/// mine. Core requires the `segwit` rule to be declared.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
let args = [serde_json::json!({ "rules": ["segwit"] })];
|
||||
let r: GetBlockTemplateResponse =
|
||||
self.call_with_retry(|c| c.call("getblocktemplate", &args))?;
|
||||
|
||||
Ok(r.transactions
|
||||
.into_iter()
|
||||
.map(|t| BlockTemplateTx {
|
||||
txid: Txid::from(t.txid),
|
||||
fee: Sats::from(t.fee),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
// Local deserialization structs for raw RPC responses
|
||||
@@ -386,3 +400,14 @@ struct TxOutResponse {
|
||||
struct TxOutScriptPubKey {
|
||||
hex: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GetBlockTemplateResponse {
|
||||
transactions: Vec<GetBlockTemplateTx>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GetBlockTemplateTx {
|
||||
txid: bitcoin::Txid,
|
||||
fee: u64,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bitcoin::ScriptBuf;
|
||||
use brk_types::Sats;
|
||||
use brk_types::{Sats, Txid};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockchainInfo {
|
||||
@@ -29,6 +29,12 @@ pub struct TxOutInfo {
|
||||
pub script_pub_key: ScriptBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockTemplateTx {
|
||||
pub txid: Txid,
|
||||
pub fee: Sats,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawMempoolEntry {
|
||||
pub vsize: u64,
|
||||
|
||||
@@ -12,7 +12,7 @@ use brk_types::{BlockHash, Height, MempoolEntryInfo, Sats, Txid, Vout};
|
||||
|
||||
pub mod backend;
|
||||
|
||||
pub use backend::{Auth, BlockHeaderInfo, BlockInfo, BlockchainInfo, TxOutInfo};
|
||||
pub use backend::{Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, TxOutInfo};
|
||||
|
||||
use backend::ClientInner;
|
||||
use tracing::{debug, info};
|
||||
@@ -201,6 +201,12 @@ impl Client {
|
||||
self.0.send_raw_transaction(hex).map(Txid::from)
|
||||
}
|
||||
|
||||
/// Transactions (txid + fee) Bitcoin Core would include in the next
|
||||
/// block it would mine, via `getblocktemplate`.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
self.0.get_block_template_txs()
|
||||
}
|
||||
|
||||
/// Checks if a block is in the main chain (has positive confirmations)
|
||||
pub fn is_in_main_chain(&self, hash: &BlockHash) -> Result<bool> {
|
||||
let block_info = self.get_block_info(hash)?;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta property="og:url" content="https://bitview.space/api" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://bitview.space/assets/favicon-196.png"
|
||||
content="https://bitview.space/assets/favicon/web-app-manifest-512x512.png"
|
||||
/>
|
||||
|
||||
<!-- Twitter Card -->
|
||||
@@ -29,7 +29,7 @@
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://bitview.space/assets/favicon-196.png"
|
||||
content="https://bitview.space/assets/favicon/web-app-manifest-512x512.png"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
@@ -111,14 +111,25 @@ impl FundedAddrData {
|
||||
}
|
||||
|
||||
/// Whether this address has received more than one output over its
|
||||
/// lifetime — the simplest proxy for address reuse (close to but not
|
||||
/// exactly "received in 2+ distinct transactions"; over-counts the rare
|
||||
/// case of multi-output funding to the same address in one tx).
|
||||
/// lifetime: the receive-side proxy for address reuse (close to but
|
||||
/// not exactly "received in 2+ distinct transactions"; over-counts
|
||||
/// the rare case of multi-output funding to the same address in one
|
||||
/// tx). Matches the industry-standard "address reuse" signal.
|
||||
#[inline]
|
||||
pub fn is_reused(&self) -> bool {
|
||||
self.funded_txo_count > 1
|
||||
}
|
||||
|
||||
/// Whether this address has spent more than one output over its
|
||||
/// lifetime: the spend-side counterpart to `is_reused`. Captures
|
||||
/// "demonstrated reuse via actual spending" and excludes addresses
|
||||
/// that received multiple outputs but have not yet been drawn from
|
||||
/// more than once.
|
||||
#[inline]
|
||||
pub fn is_respent(&self) -> bool {
|
||||
self.spent_txo_count > 1
|
||||
}
|
||||
|
||||
/// Whether this address's public key has been revealed in the chain.
|
||||
/// For P2PK33/P2PK65/P2TR the pubkey is in the locking script of any
|
||||
/// funding output; for other types it's only revealed when spending.
|
||||
@@ -145,6 +156,28 @@ impl FundedAddrData {
|
||||
}
|
||||
}
|
||||
|
||||
/// This address's contribution (in sats) to the funded-reused supply:
|
||||
/// its balance if currently funded AND reused (received ≥ 2), else 0.
|
||||
#[inline]
|
||||
pub fn reused_supply_contribution(&self) -> Sats {
|
||||
if self.is_funded() && self.is_reused() {
|
||||
self.balance()
|
||||
} else {
|
||||
Sats::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
/// This address's contribution (in sats) to the funded-respent supply:
|
||||
/// its balance if currently funded AND respent (spent ≥ 2), else 0.
|
||||
#[inline]
|
||||
pub fn respent_supply_contribution(&self) -> Sats {
|
||||
if self.is_funded() && self.is_respent() {
|
||||
self.balance()
|
||||
} else {
|
||||
Sats::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(&mut self, amount: Sats, price: Cents) {
|
||||
self.receive_outputs(amount, price, 1);
|
||||
}
|
||||
|
||||
@@ -2303,6 +2303,20 @@ function createAverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern(c
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern
|
||||
* @property {BtcCentsSatsUsdPattern} all
|
||||
* @property {BtcCentsSatsUsdPattern} p2a
|
||||
* @property {BtcCentsSatsUsdPattern} p2pk33
|
||||
* @property {BtcCentsSatsUsdPattern} p2pk65
|
||||
* @property {BtcCentsSatsUsdPattern} p2pkh
|
||||
* @property {BtcCentsSatsUsdPattern} p2sh
|
||||
* @property {BtcCentsSatsUsdPattern} p2tr
|
||||
* @property {BtcCentsSatsUsdPattern} p2wpkh
|
||||
* @property {BtcCentsSatsUsdPattern} p2wsh
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern
|
||||
* @property {SeriesPattern1<StoredI8>} index
|
||||
@@ -2371,6 +2385,39 @@ function createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, acc) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5
|
||||
* @property {BpsPercentRatioPattern2} all
|
||||
* @property {BpsPercentRatioPattern2} p2a
|
||||
* @property {BpsPercentRatioPattern2} p2pk33
|
||||
* @property {BpsPercentRatioPattern2} p2pk65
|
||||
* @property {BpsPercentRatioPattern2} p2pkh
|
||||
* @property {BpsPercentRatioPattern2} p2sh
|
||||
* @property {BpsPercentRatioPattern2} p2tr
|
||||
* @property {BpsPercentRatioPattern2} p2wpkh
|
||||
* @property {BpsPercentRatioPattern2} p2wsh
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 pattern node
|
||||
* @param {BrkClientBase} client
|
||||
* @param {string} acc - Accumulated series name
|
||||
* @returns {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5}
|
||||
*/
|
||||
function createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, acc) {
|
||||
return {
|
||||
all: createBpsPercentRatioPattern2(client, acc),
|
||||
p2a: createBpsPercentRatioPattern2(client, _p('p2a', acc)),
|
||||
p2pk33: createBpsPercentRatioPattern2(client, _p('p2pk33', acc)),
|
||||
p2pk65: createBpsPercentRatioPattern2(client, _p('p2pk65', acc)),
|
||||
p2pkh: createBpsPercentRatioPattern2(client, _p('p2pkh', acc)),
|
||||
p2sh: createBpsPercentRatioPattern2(client, _p('p2sh', acc)),
|
||||
p2tr: createBpsPercentRatioPattern2(client, _p('p2tr', acc)),
|
||||
p2wpkh: createBpsPercentRatioPattern2(client, _p('p2wpkh', acc)),
|
||||
p2wsh: createBpsPercentRatioPattern2(client, _p('p2wsh', acc)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4
|
||||
* @property {SeriesPattern1<StoredU64>} all
|
||||
@@ -2604,6 +2651,17 @@ function create_1m1w1y24hBpsPercentRatioPattern(client, acc) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActiveInputOutputSpendablePattern
|
||||
* @property {_1m1w1y24hBlockPattern} activeReusedAddrCount
|
||||
* @property {_1m1w1y24hBlockPattern2} activeReusedAddrShare
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} inputFromReusedAddrCount
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} inputFromReusedAddrShare
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} outputToReusedAddrCount
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} outputToReusedAddrShare
|
||||
* @property {_1m1w1y24hBpsPercentRatioPattern} spendableOutputToReusedAddrShare
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CapLossMvrvNetPriceProfitSoprPattern
|
||||
* @property {CentsDeltaUsdPattern} cap
|
||||
@@ -2911,6 +2969,31 @@ function createDeltaDominanceHalfInTotalPattern(client, acc) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} _1m1w1y24hBlockPattern2
|
||||
* @property {SeriesPattern1<StoredF32>} _1m
|
||||
* @property {SeriesPattern1<StoredF32>} _1w
|
||||
* @property {SeriesPattern1<StoredF32>} _1y
|
||||
* @property {SeriesPattern1<StoredF32>} _24h
|
||||
* @property {SeriesPattern18<StoredF32>} block
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a _1m1w1y24hBlockPattern2 pattern node
|
||||
* @param {BrkClientBase} client
|
||||
* @param {string} acc - Accumulated series name
|
||||
* @returns {_1m1w1y24hBlockPattern2}
|
||||
*/
|
||||
function create_1m1w1y24hBlockPattern2(client, acc) {
|
||||
return {
|
||||
_1m: createSeriesPattern1(client, _m(acc, 'average_1m')),
|
||||
_1w: createSeriesPattern1(client, _m(acc, 'average_1w')),
|
||||
_1y: createSeriesPattern1(client, _m(acc, 'average_1y')),
|
||||
_24h: createSeriesPattern1(client, _m(acc, 'average_24h')),
|
||||
block: createSeriesPattern18(client, acc),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} _1m1w1y24hBlockPattern
|
||||
* @property {SeriesPattern1<StoredF32>} _1m
|
||||
@@ -3991,6 +4074,13 @@ function createCentsSatsUsdPattern(client, acc) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} CountEventsSupplyPattern
|
||||
* @property {FundedTotalPattern} count
|
||||
* @property {ActiveInputOutputSpendablePattern} events
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern} supply
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CumulativeRollingSumPattern
|
||||
* @property {SeriesPattern1<StoredU64>} cumulative
|
||||
@@ -5113,6 +5203,7 @@ function createTransferPattern(client, acc) {
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4} total
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} new
|
||||
* @property {SeriesTree_Addrs_Reused} reused
|
||||
* @property {SeriesTree_Addrs_Respent} respent
|
||||
* @property {SeriesTree_Addrs_Exposed} exposed
|
||||
* @property {SeriesTree_Addrs_Delta} delta
|
||||
* @property {SeriesTree_Addrs_AvgAmount} avgAmount
|
||||
@@ -5224,6 +5315,7 @@ function createTransferPattern(client, acc) {
|
||||
* @typedef {Object} SeriesTree_Addrs_Reused
|
||||
* @property {FundedTotalPattern} count
|
||||
* @property {SeriesTree_Addrs_Reused_Events} events
|
||||
* @property {SeriesTree_Addrs_Reused_Supply} supply
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -5234,16 +5326,53 @@ function createTransferPattern(client, acc) {
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} inputFromReusedAddrCount
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} inputFromReusedAddrShare
|
||||
* @property {_1m1w1y24hBlockPattern} activeReusedAddrCount
|
||||
* @property {SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare} activeReusedAddrShare
|
||||
* @property {_1m1w1y24hBlockPattern2} activeReusedAddrShare
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare
|
||||
* @property {SeriesPattern18<StoredF32>} block
|
||||
* @property {SeriesPattern1<StoredF32>} _24h
|
||||
* @property {SeriesPattern1<StoredF32>} _1w
|
||||
* @property {SeriesPattern1<StoredF32>} _1m
|
||||
* @property {SeriesPattern1<StoredF32>} _1y
|
||||
* @typedef {Object} SeriesTree_Addrs_Reused_Supply
|
||||
* @property {BtcCentsSatsUsdPattern} all
|
||||
* @property {BtcCentsSatsUsdPattern} p2pk65
|
||||
* @property {BtcCentsSatsUsdPattern} p2pk33
|
||||
* @property {BtcCentsSatsUsdPattern} p2pkh
|
||||
* @property {BtcCentsSatsUsdPattern} p2sh
|
||||
* @property {BtcCentsSatsUsdPattern} p2wpkh
|
||||
* @property {BtcCentsSatsUsdPattern} p2wsh
|
||||
* @property {BtcCentsSatsUsdPattern} p2tr
|
||||
* @property {BtcCentsSatsUsdPattern} p2a
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Addrs_Respent
|
||||
* @property {FundedTotalPattern} count
|
||||
* @property {SeriesTree_Addrs_Respent_Events} events
|
||||
* @property {SeriesTree_Addrs_Respent_Supply} supply
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Addrs_Respent_Events
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} outputToReusedAddrCount
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} outputToReusedAddrShare
|
||||
* @property {_1m1w1y24hBpsPercentRatioPattern} spendableOutputToReusedAddrShare
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} inputFromReusedAddrCount
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} inputFromReusedAddrShare
|
||||
* @property {_1m1w1y24hBlockPattern} activeReusedAddrCount
|
||||
* @property {_1m1w1y24hBlockPattern2} activeReusedAddrShare
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Addrs_Respent_Supply
|
||||
* @property {BtcCentsSatsUsdPattern} all
|
||||
* @property {BtcCentsSatsUsdPattern} p2pk65
|
||||
* @property {BtcCentsSatsUsdPattern} p2pk33
|
||||
* @property {BtcCentsSatsUsdPattern} p2pkh
|
||||
* @property {BtcCentsSatsUsdPattern} p2sh
|
||||
* @property {BtcCentsSatsUsdPattern} p2wpkh
|
||||
* @property {BtcCentsSatsUsdPattern} p2wsh
|
||||
* @property {BtcCentsSatsUsdPattern} p2tr
|
||||
* @property {BtcCentsSatsUsdPattern} p2a
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -5263,20 +5392,7 @@ function createTransferPattern(client, acc) {
|
||||
* @property {BtcCentsSatsUsdPattern} p2wsh
|
||||
* @property {BtcCentsSatsUsdPattern} p2tr
|
||||
* @property {BtcCentsSatsUsdPattern} p2a
|
||||
* @property {SeriesTree_Addrs_Exposed_Supply_Share} share
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Addrs_Exposed_Supply_Share
|
||||
* @property {BpsPercentRatioPattern2} all
|
||||
* @property {BpsPercentRatioPattern2} p2pk65
|
||||
* @property {BpsPercentRatioPattern2} p2pk33
|
||||
* @property {BpsPercentRatioPattern2} p2pkh
|
||||
* @property {BpsPercentRatioPattern2} p2sh
|
||||
* @property {BpsPercentRatioPattern2} p2wpkh
|
||||
* @property {BpsPercentRatioPattern2} p2wsh
|
||||
* @property {BpsPercentRatioPattern2} p2tr
|
||||
* @property {BpsPercentRatioPattern2} p2a
|
||||
* @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -8631,38 +8747,58 @@ class BrkClient extends BrkClientBase {
|
||||
inputFromReusedAddrCount: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(this, 'input_from_reused_addr_count'),
|
||||
inputFromReusedAddrShare: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(this, 'input_from_reused_addr_share'),
|
||||
activeReusedAddrCount: create_1m1w1y24hBlockPattern(this, 'active_reused_addr_count'),
|
||||
activeReusedAddrShare: {
|
||||
block: createSeriesPattern18(this, 'active_reused_addr_share'),
|
||||
_24h: createSeriesPattern1(this, 'active_reused_addr_share_average_24h'),
|
||||
_1w: createSeriesPattern1(this, 'active_reused_addr_share_average_1w'),
|
||||
_1m: createSeriesPattern1(this, 'active_reused_addr_share_average_1m'),
|
||||
_1y: createSeriesPattern1(this, 'active_reused_addr_share_average_1y'),
|
||||
},
|
||||
activeReusedAddrShare: create_1m1w1y24hBlockPattern2(this, 'active_reused_addr_share'),
|
||||
},
|
||||
supply: {
|
||||
all: createBtcCentsSatsUsdPattern(this, 'reused_addr_supply'),
|
||||
p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_reused_addr_supply'),
|
||||
p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_reused_addr_supply'),
|
||||
p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_reused_addr_supply'),
|
||||
p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_reused_addr_supply'),
|
||||
p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_reused_addr_supply'),
|
||||
p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_reused_addr_supply'),
|
||||
p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_reused_addr_supply'),
|
||||
p2a: createBtcCentsSatsUsdPattern(this, 'p2a_reused_addr_supply'),
|
||||
share: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(this, 'reused_addr_supply_share'),
|
||||
},
|
||||
},
|
||||
respent: {
|
||||
count: createFundedTotalPattern(this, 'respent_addr_count'),
|
||||
events: {
|
||||
outputToReusedAddrCount: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(this, 'output_to_respent_addr_count'),
|
||||
outputToReusedAddrShare: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(this, 'output_to_respent_addr_share'),
|
||||
spendableOutputToReusedAddrShare: create_1m1w1y24hBpsPercentRatioPattern(this, 'spendable_output_to_respent_addr_share'),
|
||||
inputFromReusedAddrCount: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(this, 'input_from_respent_addr_count'),
|
||||
inputFromReusedAddrShare: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(this, 'input_from_respent_addr_share'),
|
||||
activeReusedAddrCount: create_1m1w1y24hBlockPattern(this, 'active_respent_addr_count'),
|
||||
activeReusedAddrShare: create_1m1w1y24hBlockPattern2(this, 'active_respent_addr_share'),
|
||||
},
|
||||
supply: {
|
||||
all: createBtcCentsSatsUsdPattern(this, 'respent_addr_supply'),
|
||||
p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_respent_addr_supply'),
|
||||
p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_respent_addr_supply'),
|
||||
p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_respent_addr_supply'),
|
||||
p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_respent_addr_supply'),
|
||||
p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_respent_addr_supply'),
|
||||
p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_respent_addr_supply'),
|
||||
p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_respent_addr_supply'),
|
||||
p2a: createBtcCentsSatsUsdPattern(this, 'p2a_respent_addr_supply'),
|
||||
share: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(this, 'respent_addr_supply_share'),
|
||||
},
|
||||
},
|
||||
exposed: {
|
||||
count: createFundedTotalPattern(this, 'exposed_addr_count'),
|
||||
supply: {
|
||||
all: createBtcCentsSatsUsdPattern(this, 'exposed_supply'),
|
||||
p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_exposed_supply'),
|
||||
p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_exposed_supply'),
|
||||
p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_exposed_supply'),
|
||||
p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_exposed_supply'),
|
||||
p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_exposed_supply'),
|
||||
p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_exposed_supply'),
|
||||
p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_exposed_supply'),
|
||||
p2a: createBtcCentsSatsUsdPattern(this, 'p2a_exposed_supply'),
|
||||
share: {
|
||||
all: createBpsPercentRatioPattern2(this, 'exposed_supply_share'),
|
||||
p2pk65: createBpsPercentRatioPattern2(this, 'p2pk65_exposed_supply_share'),
|
||||
p2pk33: createBpsPercentRatioPattern2(this, 'p2pk33_exposed_supply_share'),
|
||||
p2pkh: createBpsPercentRatioPattern2(this, 'p2pkh_exposed_supply_share'),
|
||||
p2sh: createBpsPercentRatioPattern2(this, 'p2sh_exposed_supply_share'),
|
||||
p2wpkh: createBpsPercentRatioPattern2(this, 'p2wpkh_exposed_supply_share'),
|
||||
p2wsh: createBpsPercentRatioPattern2(this, 'p2wsh_exposed_supply_share'),
|
||||
p2tr: createBpsPercentRatioPattern2(this, 'p2tr_exposed_supply_share'),
|
||||
p2a: createBpsPercentRatioPattern2(this, 'p2a_exposed_supply_share'),
|
||||
},
|
||||
all: createBtcCentsSatsUsdPattern(this, 'exposed_addr_supply'),
|
||||
p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_exposed_addr_supply'),
|
||||
p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_exposed_addr_supply'),
|
||||
p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_exposed_addr_supply'),
|
||||
p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_exposed_addr_supply'),
|
||||
p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_exposed_addr_supply'),
|
||||
p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_exposed_addr_supply'),
|
||||
p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_exposed_addr_supply'),
|
||||
p2a: createBtcCentsSatsUsdPattern(this, 'p2a_exposed_addr_supply'),
|
||||
share: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(this, 'exposed_addr_supply_share'),
|
||||
},
|
||||
},
|
||||
delta: {
|
||||
|
||||
@@ -2767,6 +2767,10 @@ class AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern(Generic[T]
|
||||
self.pct90: _1m1w1y24hPattern[T] = _1m1w1y24hPattern(client, _m(acc, 'pct90'))
|
||||
self.sum: _1m1w1y24hPattern[T] = _1m1w1y24hPattern(client, _m(acc, 'sum'))
|
||||
|
||||
class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
pass
|
||||
|
||||
class IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
|
||||
@@ -2798,6 +2802,21 @@ class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6:
|
||||
self.p2wpkh: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _p('p2wpkh', acc))
|
||||
self.p2wsh: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _p('p2wsh', acc))
|
||||
|
||||
class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, acc: str):
|
||||
"""Create pattern node with accumulated series name."""
|
||||
self.all: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, acc)
|
||||
self.p2a: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2a', acc))
|
||||
self.p2pk33: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2pk33', acc))
|
||||
self.p2pk65: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2pk65', acc))
|
||||
self.p2pkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2pkh', acc))
|
||||
self.p2sh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2sh', acc))
|
||||
self.p2tr: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2tr', acc))
|
||||
self.p2wpkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2wpkh', acc))
|
||||
self.p2wsh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2wsh', acc))
|
||||
|
||||
class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
|
||||
@@ -2902,6 +2921,10 @@ class _1m1w1y24hBpsPercentRatioPattern:
|
||||
self.percent: SeriesPattern1[StoredF32] = SeriesPattern1(client, acc)
|
||||
self.ratio: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'ratio'))
|
||||
|
||||
class ActiveInputOutputSpendablePattern:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
pass
|
||||
|
||||
class CapLossMvrvNetPriceProfitSoprPattern:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
|
||||
@@ -3038,6 +3061,17 @@ class DeltaDominanceHalfInTotalPattern:
|
||||
self.in_profit: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, _m(acc, 'in_profit'))
|
||||
self.total: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, acc)
|
||||
|
||||
class _1m1w1y24hBlockPattern2:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, acc: str):
|
||||
"""Create pattern node with accumulated series name."""
|
||||
self._1m: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_1m'))
|
||||
self._1w: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_1w'))
|
||||
self._1y: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_1y'))
|
||||
self._24h: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_24h'))
|
||||
self.block: SeriesPattern18[StoredF32] = SeriesPattern18(client, acc)
|
||||
|
||||
class _1m1w1y24hBlockPattern:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
|
||||
@@ -3506,6 +3540,10 @@ class CentsSatsUsdPattern:
|
||||
self.sats: SeriesPattern1[SatsFract] = SeriesPattern1(client, _m(acc, 'sats'))
|
||||
self.usd: SeriesPattern1[Dollars] = SeriesPattern1(client, acc)
|
||||
|
||||
class CountEventsSupplyPattern:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
pass
|
||||
|
||||
class CumulativeRollingSumPattern:
|
||||
"""Pattern struct for repeated tree structure."""
|
||||
|
||||
@@ -4316,16 +4354,6 @@ class SeriesTree_Addrs_Activity:
|
||||
self.p2tr: ActiveBidirectionalReactivatedReceivingSendingPattern = ActiveBidirectionalReactivatedReceivingSendingPattern(client, 'p2tr')
|
||||
self.p2a: ActiveBidirectionalReactivatedReceivingSendingPattern = ActiveBidirectionalReactivatedReceivingSendingPattern(client, 'p2a')
|
||||
|
||||
class SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.block: SeriesPattern18[StoredF32] = SeriesPattern18(client, 'active_reused_addr_share')
|
||||
self._24h: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_24h')
|
||||
self._1w: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_1w')
|
||||
self._1m: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_1m')
|
||||
self._1y: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_1y')
|
||||
|
||||
class SeriesTree_Addrs_Reused_Events:
|
||||
"""Series tree node."""
|
||||
|
||||
@@ -4336,7 +4364,22 @@ class SeriesTree_Addrs_Reused_Events:
|
||||
self.input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'input_from_reused_addr_count')
|
||||
self.input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(client, 'input_from_reused_addr_share')
|
||||
self.active_reused_addr_count: _1m1w1y24hBlockPattern = _1m1w1y24hBlockPattern(client, 'active_reused_addr_count')
|
||||
self.active_reused_addr_share: SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare = SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare(client)
|
||||
self.active_reused_addr_share: _1m1w1y24hBlockPattern2 = _1m1w1y24hBlockPattern2(client, 'active_reused_addr_share')
|
||||
|
||||
class SeriesTree_Addrs_Reused_Supply:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'reused_addr_supply')
|
||||
self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_reused_addr_supply')
|
||||
self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_reused_addr_supply')
|
||||
self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_reused_addr_supply')
|
||||
self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_reused_addr_supply')
|
||||
self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_reused_addr_supply')
|
||||
self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_reused_addr_supply')
|
||||
self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_reused_addr_supply')
|
||||
self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_reused_addr_supply')
|
||||
self.share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, 'reused_addr_supply_share')
|
||||
|
||||
class SeriesTree_Addrs_Reused:
|
||||
"""Series tree node."""
|
||||
@@ -4344,35 +4387,57 @@ class SeriesTree_Addrs_Reused:
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.count: FundedTotalPattern = FundedTotalPattern(client, 'reused_addr_count')
|
||||
self.events: SeriesTree_Addrs_Reused_Events = SeriesTree_Addrs_Reused_Events(client)
|
||||
self.supply: SeriesTree_Addrs_Reused_Supply = SeriesTree_Addrs_Reused_Supply(client)
|
||||
|
||||
class SeriesTree_Addrs_Exposed_Supply_Share:
|
||||
class SeriesTree_Addrs_Respent_Events:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.all: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'exposed_supply_share')
|
||||
self.p2pk65: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2pk65_exposed_supply_share')
|
||||
self.p2pk33: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2pk33_exposed_supply_share')
|
||||
self.p2pkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2pkh_exposed_supply_share')
|
||||
self.p2sh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2sh_exposed_supply_share')
|
||||
self.p2wpkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2wpkh_exposed_supply_share')
|
||||
self.p2wsh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2wsh_exposed_supply_share')
|
||||
self.p2tr: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2tr_exposed_supply_share')
|
||||
self.p2a: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2a_exposed_supply_share')
|
||||
self.output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'output_to_respent_addr_count')
|
||||
self.output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(client, 'output_to_respent_addr_share')
|
||||
self.spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern = _1m1w1y24hBpsPercentRatioPattern(client, 'spendable_output_to_respent_addr_share')
|
||||
self.input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'input_from_respent_addr_count')
|
||||
self.input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(client, 'input_from_respent_addr_share')
|
||||
self.active_reused_addr_count: _1m1w1y24hBlockPattern = _1m1w1y24hBlockPattern(client, 'active_respent_addr_count')
|
||||
self.active_reused_addr_share: _1m1w1y24hBlockPattern2 = _1m1w1y24hBlockPattern2(client, 'active_respent_addr_share')
|
||||
|
||||
class SeriesTree_Addrs_Respent_Supply:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'respent_addr_supply')
|
||||
self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_respent_addr_supply')
|
||||
self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_respent_addr_supply')
|
||||
self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_respent_addr_supply')
|
||||
self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_respent_addr_supply')
|
||||
self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_respent_addr_supply')
|
||||
self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_respent_addr_supply')
|
||||
self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_respent_addr_supply')
|
||||
self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_respent_addr_supply')
|
||||
self.share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, 'respent_addr_supply_share')
|
||||
|
||||
class SeriesTree_Addrs_Respent:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.count: FundedTotalPattern = FundedTotalPattern(client, 'respent_addr_count')
|
||||
self.events: SeriesTree_Addrs_Respent_Events = SeriesTree_Addrs_Respent_Events(client)
|
||||
self.supply: SeriesTree_Addrs_Respent_Supply = SeriesTree_Addrs_Respent_Supply(client)
|
||||
|
||||
class SeriesTree_Addrs_Exposed_Supply:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'exposed_supply')
|
||||
self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_exposed_supply')
|
||||
self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_exposed_supply')
|
||||
self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_exposed_supply')
|
||||
self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_exposed_supply')
|
||||
self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_exposed_supply')
|
||||
self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_exposed_supply')
|
||||
self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_exposed_supply')
|
||||
self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_exposed_supply')
|
||||
self.share: SeriesTree_Addrs_Exposed_Supply_Share = SeriesTree_Addrs_Exposed_Supply_Share(client)
|
||||
self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'exposed_addr_supply')
|
||||
self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_exposed_addr_supply')
|
||||
self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_exposed_addr_supply')
|
||||
self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_exposed_addr_supply')
|
||||
self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_exposed_addr_supply')
|
||||
self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_exposed_addr_supply')
|
||||
self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_exposed_addr_supply')
|
||||
self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_exposed_addr_supply')
|
||||
self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_exposed_addr_supply')
|
||||
self.share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, 'exposed_addr_supply_share')
|
||||
|
||||
class SeriesTree_Addrs_Exposed:
|
||||
"""Series tree node."""
|
||||
@@ -4422,6 +4487,7 @@ class SeriesTree_Addrs:
|
||||
self.total: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4(client, 'total_addr_count')
|
||||
self.new: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'new_addr_count')
|
||||
self.reused: SeriesTree_Addrs_Reused = SeriesTree_Addrs_Reused(client)
|
||||
self.respent: SeriesTree_Addrs_Respent = SeriesTree_Addrs_Respent(client)
|
||||
self.exposed: SeriesTree_Addrs_Exposed = SeriesTree_Addrs_Exposed(client)
|
||||
self.delta: SeriesTree_Addrs_Delta = SeriesTree_Addrs_Delta(client)
|
||||
self.avg_amount: SeriesTree_Addrs_AvgAmount = SeriesTree_Addrs_AvgAmount(client)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"type": "openapi",
|
||||
"url": "https://bitview.space/openapi.json"
|
||||
},
|
||||
"logo_url": "https://bitview.space/assets/favicon-196.png",
|
||||
"logo_url": "https://bitview.space/assets/favicon/web-app-manifest-512x512.png",
|
||||
"contact_email": "hello@bitcoinresearchkit.org",
|
||||
"legal_info_url": "https://github.com/bitcoinresearchkit/brk/blob/main/docs/LICENSE.md"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 511 B |
|
Before Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,43 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview" width="173.2" height="200"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><title>bitview</title><style>
|
||||
svg {
|
||||
--fill: 0.5;
|
||||
--face-top: oklch(67.64% 0.191 44.41);
|
||||
--face-left: oklch(64.34% 0.191 44.41);
|
||||
--face-right: oklch(61.04% 0.191 44.41);
|
||||
--face-bottom: oklch(57.74% 0.191 44.41);
|
||||
}
|
||||
svg[data-theme="light"] {
|
||||
--face-top: oklch(90% 0 0);
|
||||
--face-left: oklch(86.7% 0 0);
|
||||
--face-right: oklch(83.4% 0 0);
|
||||
--face-bottom: oklch(80.1% 0 0);
|
||||
}
|
||||
svg[data-theme="dark"] {
|
||||
--face-top: oklch(26.6% 0 0);
|
||||
--face-left: oklch(23.3% 0 0);
|
||||
--face-right: oklch(20% 0 0);
|
||||
--face-bottom: oklch(10.1% 0 0);
|
||||
}
|
||||
.glass { fill-opacity: 0.4; }
|
||||
.glass-top, .liquid-top { fill: var(--face-top); }
|
||||
.glass-right, .liquid-right { fill: var(--face-right); }
|
||||
.glass-left, .liquid-left { fill: var(--face-left); }
|
||||
.glass-bottom { fill: var(--face-bottom); }
|
||||
.glass-rear-left { fill: var(--face-top); }
|
||||
.glass-rear-right { fill: var(--face-left); }
|
||||
.liquid {
|
||||
transform-box: view-box;
|
||||
transform: translateY(calc((1 - var(--fill)) * 50%));
|
||||
opacity: ceil(var(--fill));
|
||||
}
|
||||
</style><defs>
|
||||
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
|
||||
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"></polygon>
|
||||
</clipPath>
|
||||
</defs><polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"></polygon><polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"></polygon><polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"></polygon><g clip-path="url(#hex)">
|
||||
<g class="liquid">
|
||||
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"></polygon>
|
||||
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"></polygon>
|
||||
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"></polygon>
|
||||
</g>
|
||||
</g><polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"></polygon><polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"></polygon><polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"></polygon></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "bitview",
|
||||
"short_name": "bitview",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#EEE",
|
||||
"background_color": "#EEE",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -26,16 +26,9 @@
|
||||
--cube: calc(var(--cube-rem) * 1rem);
|
||||
--cube-px: calc(var(--cube-rem) * 16);
|
||||
--face-scale: calc(100 / var(--cube-px));
|
||||
--orange: oklch(67.64% 0.191 44.41);
|
||||
--white: oklch(95% 0 0);
|
||||
--black: oklch(15% 0 0);
|
||||
--light-gray: oklch(90% 0 0);
|
||||
--dark-gray: oklch(20% 0 0);
|
||||
--border-color: light-dark(var(--light-gray), var(--dark-gray));
|
||||
--background-color: light-dark(var(--white), var(--black));
|
||||
--fill: 0.5;
|
||||
--empty-alpha: 0.4;
|
||||
--face-step: 0.033;
|
||||
--iso-scale: 0.866;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
@@ -113,6 +106,8 @@
|
||||
font-weight: 500;
|
||||
pointer-events: none;
|
||||
}
|
||||
.face-text { color: var(--black); }
|
||||
svg[data-theme="dark"] .face-text { color: var(--white); }
|
||||
.face-text.top,
|
||||
.face-text.right {
|
||||
display: flex;
|
||||
@@ -165,51 +160,65 @@
|
||||
.face-text .pool img.marapool { content: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgdmlld0JveD0iMCAwIDI1NiAyNTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0wIDMuNjAwOEMwIDIuMzQzMTMgMCAxLjcxNDMgMC4yNDQ3NTcgMS4yMzM5NEMwLjQ2MDA1MiAwLjgxMTQgMC44MDM1ODcgMC40Njc4NjQgMS4yMjYxMyAwLjI1MjU3QzEuNzA2NDkgMC4wMDc4MTI1IDIuMzM1MzIgMC4wMDc4MTI1IDMuNTkyOTggMC4wMDc4MTI1SDI1Mi40MDdDMjUzLjY2NSAwLjAwNzgxMjUgMjU0LjI5NCAwLjAwNzgxMjUgMjU0Ljc3NCAwLjI1MjU3QzI1NS4xOTYgMC40Njc4NjQgMjU1LjU0IDAuODExNCAyNTUuNzU1IDEuMjMzOTRDMjU2IDEuNzE0MyAyNTYgMi4zNDMxMyAyNTYgMy42MDA4VjI1Mi40MTVDMjU2IDI1My42NzIgMjU2IDI1NC4zMDEgMjU1Ljc1NSAyNTQuNzgyQzI1NS41NCAyNTUuMjA0IDI1NS4xOTYgMjU1LjU0OCAyNTQuNzc0IDI1NS43NjNDMjU0LjI5NCAyNTYuMDA4IDI1My42NjUgMjU2LjAwOCAyNTIuNDA3IDI1Ni4wMDhIMy41OTI5OUMyLjMzNTMyIDI1Ni4wMDggMS43MDY0OSAyNTYuMDA4IDEuMjI2MTMgMjU1Ljc2M0MwLjgwMzU4NyAyNTUuNTQ4IDAuNDYwMDUyIDI1NS4yMDQgMC4yNDQ3NTcgMjU0Ljc4MkMwIDI1NC4zMDEgMCAyNTMuNjcyIDAgMjUyLjQxNVYzLjYwMDhaIiBmaWxsPSIjMjcyNTI1Ii8+CjxwYXRoIGQ9Ik01OS4yODUgNDguMDYyNUM1OC43ODg5IDQ4LjA2MjUgNTguMzg2NyA0OC40NjQ3IDU4LjM4NjcgNDguOTYwN1YxNTkuNDQ1QzU4LjM4NjcgMTU5Ljk0MSA1OC43ODg5IDE2MC4zNDMgNTkuMjg1IDE2MC4zNDNIODAuMDc0OUM4MC41NzA5IDE2MC4zNDMgODAuOTczMSAxNTkuOTQxIDgwLjk3MzEgMTU5LjQ0NVY4Mi4zODlIODQuNjMzNUwxMTcuNjQ5IDE1OS43OTdDMTE3Ljc5IDE2MC4xMjggMTE4LjExNiAxNjAuMzQzIDExOC40NzUgMTYwLjM0M0gxMzcuODA5QzEzOC4xNjkgMTYwLjM0MyAxMzguNDk0IDE2MC4xMjggMTM4LjYzNSAxNTkuNzk3TDE3MS42NTEgODIuMzg5SDE3NS4zMTFWMTU5LjQ0NUMxNzUuMzExIDE1OS45NDEgMTc1LjcxNCAxNjAuMzQzIDE3Ni4yMSAxNjAuMzQzSDE5N0MxOTcuNDk2IDE2MC4zNDMgMTk3Ljg5OCAxNTkuOTQxIDE5Ny44OTggMTU5LjQ0NVY0OC45NjA3QzE5Ny44OTggNDguNDY0NyAxOTcuNDk2IDQ4LjA2MjUgMTk3IDQ4LjA2MjVIMTY0LjI5QzE2My45MzEgNDguMDYyNSAxNjMuNjA1IDQ4LjI3NzMgMTYzLjQ2NCA0OC42MDg0TDEyOS45NzIgMTI3LjE0SDEyNi4zMTJMOTIuODIwMiA0OC42MDg0QzkyLjY3OSA0OC4yNzczIDkyLjM1MzkgNDguMDYyNSA5MS45OTQgNDguMDYyNUg1OS4yODVaIiBmaWxsPSIjRUVFQ0U4Ii8+CjxwYXRoIGQ9Ik01OC4zODY3IDE5NC45MjZDNTguMzg2NyAxOTQuNDMgNTguNzg4OSAxOTQuMDI3IDU5LjI4NSAxOTQuMDI3SDE5Ni45OTlDMTk3LjQ5NiAxOTQuMDI3IDE5Ny44OTggMTk0LjQzIDE5Ny44OTggMTk0LjkyNlYyMTUuNTg1QzE5Ny44OTggMjE2LjA4MSAxOTcuNDk2IDIxNi40ODQgMTk2Ljk5OSAyMTYuNDg0SDU5LjI4NUM1OC43ODg5IDIxNi40ODQgNTguMzg2NyAyMTYuMDgxIDU4LjM4NjcgMjE1LjU4NVYxOTQuOTI2WiIgZmlsbD0iI0YyQTkwMCIvPgo8L3N2Zz4K"); }
|
||||
.face-text .pool img.unknown { content: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjUuMjcgMyAxMy40OCAxOCI+IDxwYXRoIGQ9Ik01LjY0MTcgMTguMzY5NkM1LjE1ODM0IDE3Ljg4NzEgNS4xNTgzMyAxNy4xMTMgNS42NDE2OSAxNi42MzA0QzcuMjY5OTIgMTUuMDA1MSA5LjUxNzYxIDE0IDEyIDE0QzE0LjQ4MjUgMTQgMTYuNzMwMSAxNS4wMDUgMTguMzU4MyAxNi42MzA0QzE4Ljg0MTcgMTcuMTEyOSAxOC44NDE3IDE3Ljg4NzEgMTguMzU4NCAxOC4zNjk2QzE2LjczMDEgMTkuOTk0OSAxNC40ODI0IDIxIDEyIDIxQzkuNTE3NTkgMjEgNy4yNjk5MyAxOS45OTUgNS42NDE3IDE4LjM2OTZaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNOC41MDAwMyA3LjVIMTUuNVY4SDE2QzE2IDEwLjYzNzIgMTQuMzE4OCAxMyAxMiAxM0M5LjY4MTIzIDEzIDguMDAwMDMgMTAuNjM3MiA4LjAwMDAzIDhIOC41MDAwM1Y3LjVaTTkuMDIyNzMgOC41QzkuMjEyNjYgMTAuNTY3OCAxMC41NjU4IDEyIDEyIDEyQzEzLjQzNDMgMTIgMTQuNzg3NCAxMC41Njc4IDE0Ljk3NzMgOC41SDkuMDIyNzNaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTIgM0MxNC40ODUzIDMgMTYuNSA1LjIzODU4IDE2LjUgOEg3LjUwMDAzQzcuNTAwMDMgNS4yMzg1OCA5LjUxNDc1IDMgMTIgM1pNMTIgNy41QzEyLjgyODUgNy41IDEzLjUgNi44Mjg0MyAxMy41IDZDMTMuNSA1LjE3MTU3IDEyLjgyODUgNC41IDEyIDQuNUMxMS4xNzE2IDQuNSAxMC41IDUuMTcxNTcgMTAuNSA2QzEwLjUgNi44Mjg0MyAxMS4xNzE2IDcuNSAxMiA3LjVaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNi41MDAwMyA4LjVDNi41MDAwMyA4LjIyMzg2IDYuNzIzODkgOCA3LjAwMDAzIDhIMTdDMTcuMjc2MiA4IDE3LjUgOC4yMjM4NiAxNy41IDguNUMxNy41IDguNzc2MTQgMTcuMjc2MiA5IDE3IDlINy4wMDAwM0M2LjcyMzg5IDkgNi41MDAwMyA4Ljc3NjE0IDYuNTAwMDMgOC41WiIgZmlsbD0iI2I0YjRiNCI+PC9wYXRoPiA8L3N2Zz4="); }
|
||||
|
||||
/* ---- cube face colors (derived from --face-color, same logic
|
||||
as website/styles/panes/explorer.css) ---- */
|
||||
svg.cube {
|
||||
--face-color: var(--orange);
|
||||
--face-right: light-dark(
|
||||
oklch(from var(--face-color) calc(l - var(--face-step) * 2) c h),
|
||||
var(--face-color)
|
||||
);
|
||||
--face-left: light-dark(
|
||||
oklch(from var(--face-color) calc(l - var(--face-step)) c h),
|
||||
oklch(from var(--face-color) calc(l + var(--face-step)) c h)
|
||||
);
|
||||
--face-top: light-dark(
|
||||
var(--face-color),
|
||||
oklch(from var(--face-color) calc(l + var(--face-step) * 2) c h)
|
||||
);
|
||||
--face-bottom: oklch(from var(--face-color) calc(l - var(--face-step) * 3) c h);
|
||||
color: light-dark(var(--black), var(--white));
|
||||
}
|
||||
|
||||
/* Glass polygons: translucent. Liquid polygons: opaque. Rear-face
|
||||
colors mirror demo.html (bottom → face-bottom, rear-left →
|
||||
face-top, rear-right → face-left). */
|
||||
svg.cube .glass { fill-opacity: var(--empty-alpha); }
|
||||
|
||||
svg.cube .glass-top, svg.cube .liquid-top { fill: var(--face-top); }
|
||||
svg.cube .glass-right, svg.cube .liquid-right { fill: var(--face-right); }
|
||||
svg.cube .glass-left, svg.cube .liquid-left { fill: var(--face-left); }
|
||||
svg.cube .glass-bottom { fill: var(--face-bottom); }
|
||||
svg.cube .glass-rear-left { fill: var(--face-top); }
|
||||
svg.cube .glass-rear-right { fill: var(--face-left); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<!-- SVG shell. All polygons + text-face transforms are generated
|
||||
in JS from the cube constants (ISO, OX, OY, CUBE), so the
|
||||
geometry has a single source of truth. DOM order defines
|
||||
z-order: rear polygons, liquid, front polygons, text. -->
|
||||
<template id="logo-template">
|
||||
<div class="logo-slot">
|
||||
<svg class="cube"></svg>
|
||||
</div>
|
||||
</template>
|
||||
<template id="logo-template"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
|
||||
<title>bitview</title>
|
||||
<style>
|
||||
svg {
|
||||
--fill: 0.5;
|
||||
--face-top: oklch(67.64% 0.191 44.41);
|
||||
--face-left: oklch(64.34% 0.191 44.41);
|
||||
--face-right: oklch(61.04% 0.191 44.41);
|
||||
--face-bottom: oklch(57.74% 0.191 44.41);
|
||||
}
|
||||
svg[data-theme="light"] {
|
||||
--face-top: oklch(90% 0 0);
|
||||
--face-left: oklch(86.7% 0 0);
|
||||
--face-right: oklch(83.4% 0 0);
|
||||
--face-bottom: oklch(80.1% 0 0);
|
||||
}
|
||||
svg[data-theme="dark"] {
|
||||
--face-top: oklch(26.6% 0 0);
|
||||
--face-left: oklch(23.3% 0 0);
|
||||
--face-right: oklch(20% 0 0);
|
||||
--face-bottom: oklch(10.1% 0 0);
|
||||
}
|
||||
.glass { fill-opacity: 0.4; }
|
||||
.glass-top, .liquid-top { fill: var(--face-top); }
|
||||
.glass-right, .liquid-right { fill: var(--face-right); }
|
||||
.glass-left, .liquid-left { fill: var(--face-left); }
|
||||
.glass-bottom { fill: var(--face-bottom); }
|
||||
.glass-rear-left { fill: var(--face-top); }
|
||||
.glass-rear-right { fill: var(--face-left); }
|
||||
.liquid {
|
||||
transform-box: view-box;
|
||||
transform: translateY(calc((1 - var(--fill)) * 50%));
|
||||
opacity: ceil(var(--fill));
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
|
||||
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
|
||||
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
|
||||
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
|
||||
<g clip-path="url(#hex)">
|
||||
<g class="liquid">
|
||||
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
|
||||
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</g>
|
||||
</g>
|
||||
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
|
||||
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</svg></template>
|
||||
|
||||
<div class="wrap">
|
||||
<section>
|
||||
<div class="row" id="row"></div>
|
||||
</section>
|
||||
@@ -224,61 +233,18 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- Cube geometry: everything derives from these 4 constants.
|
||||
// Change ISO/OX/OY/CUBE and the entire cube reprojects. ---
|
||||
<script type="module">
|
||||
const ISO = 0.866, OX = 0.3, OY = 0.6, CUBE = 100;
|
||||
const W = 2 * ISO * CUBE, H = 2 * CUBE; // viewBox 173.2 × 200
|
||||
const W = 2 * ISO * CUBE, H = 2 * CUBE;
|
||||
|
||||
// Hex vertices (iso projection of the 8 cube vertices; parallel
|
||||
// pairs collapse into V.C, then these 7 outline the silhouette).
|
||||
const V = {
|
||||
T: [W/2, 0 ],
|
||||
UR: [W, H/4 ], UL: [0, H/4 ],
|
||||
C: [W/2, H/2 ],
|
||||
LR: [W, 3*H/4 ], LL: [0, 3*H/4 ],
|
||||
B: [W/2, H ],
|
||||
};
|
||||
const lerp = ([ax,ay], [bx,by], t) => [ax + (bx-ax)*t, ay + (by-ay)*t];
|
||||
|
||||
// Static glass polygons. DOM order sets z-order (rear polygons
|
||||
// first, then liquid, then front). All share the .glass class so
|
||||
// the single fill-opacity rule applies to both rear and front.
|
||||
const GLASS_REAR = [
|
||||
{ cls: 'glass glass-bottom', pts: [V.C, V.LR, V.B, V.LL] },
|
||||
{ cls: 'glass glass-rear-left', pts: [V.C, V.LL, V.UL, V.T] },
|
||||
{ cls: 'glass glass-rear-right', pts: [V.C, V.LR, V.UR, V.T] },
|
||||
];
|
||||
const GLASS_FRONT = [
|
||||
{ cls: 'glass glass-top', pts: [V.UL, V.T, V.UR, V.C] },
|
||||
{ cls: 'glass glass-right', pts: [V.UR, V.LR, V.B, V.C] },
|
||||
{ cls: 'glass glass-left', pts: [V.UL, V.LL, V.B, V.C] },
|
||||
];
|
||||
|
||||
// Liquid surface at fill f is a linear interp of the cube's
|
||||
// bottom-face corners (f=0) toward its top-face corners (f=1).
|
||||
// The 3 liquid sub-polygons clip the visible front faces at it.
|
||||
const surface = (f) => ({
|
||||
back: lerp(V.C, V.T, f),
|
||||
tr: lerp(V.LR, V.UR, f),
|
||||
front: lerp(V.B, V.C, f),
|
||||
tl: lerp(V.LL, V.UL, f),
|
||||
});
|
||||
const liquidPolygons = (f) => {
|
||||
if (f <= 0) return [];
|
||||
const s = surface(f);
|
||||
return [
|
||||
{ cls: 'liquid-top', pts: [s.back, s.tr, s.front, s.tl] },
|
||||
{ cls: 'liquid-right', pts: [s.tr, V.LR, V.B, s.front] },
|
||||
{ cls: 'liquid-left', pts: [s.tl, V.LL, V.B, s.front] },
|
||||
];
|
||||
const SVGNS = 'http://www.w3.org/2000/svg';
|
||||
const XHTMLNS = 'http://www.w3.org/1999/xhtml';
|
||||
const svgEl = (tag, attrs = {}) => {
|
||||
const e = document.createElementNS(SVGNS, tag);
|
||||
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
|
||||
return e;
|
||||
};
|
||||
|
||||
// Face-text transforms: outer translate re-anchors the cube
|
||||
// origin (HTML demo's (0,0)) to the viewBox, then per-face
|
||||
// `rotate skewX translate scaleY(ISO)` matches the HTML demo.
|
||||
// Composed into a single transform per face and set directly on
|
||||
// each <foreignObject> — no wrapping <g>.
|
||||
const OUTER_TF =
|
||||
`translate(${W/2 - ISO*(OX+1)*CUBE} ${H/2 - (0.5*(OX+1) + OY/ISO)*CUBE})`;
|
||||
const tf = (rot, skew, tx, ty) =>
|
||||
@@ -289,38 +255,15 @@
|
||||
['left', tf( 30, 30, OX * CUBE, OY * CUBE)],
|
||||
];
|
||||
|
||||
const SVGNS = 'http://www.w3.org/2000/svg';
|
||||
const XHTMLNS = 'http://www.w3.org/1999/xhtml';
|
||||
const svgEl = (tag, attrs = {}) => {
|
||||
const e = document.createElementNS(SVGNS, tag);
|
||||
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
|
||||
return e;
|
||||
};
|
||||
const ptsAttr = (pts) => pts.map(p => p.join(',')).join(' ');
|
||||
const addPoly = (parent, cls, pts) =>
|
||||
parent.appendChild(svgEl('polygon', { class: cls, points: ptsAttr(pts) }));
|
||||
const logoSvg = document.getElementById('logo-template').content.querySelector('svg');
|
||||
|
||||
const template = document.getElementById('logo-template');
|
||||
|
||||
const makeLogo = ({ fill = 0.5, faceColor, cubeScheme } = {}) => {
|
||||
const slot = template.content.cloneNode(true).firstElementChild;
|
||||
const svg = slot.querySelector('svg');
|
||||
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
||||
svg.style.setProperty('--fill', String(fill));
|
||||
if (faceColor) svg.style.setProperty('--face-color', faceColor);
|
||||
svg.style.setProperty('color-scheme', cubeScheme ?? 'light');
|
||||
|
||||
for (const f of GLASS_REAR) addPoly(svg, f.cls, f.pts);
|
||||
for (const f of liquidPolygons(fill)) addPoly(svg, f.cls, f.pts);
|
||||
for (const f of GLASS_FRONT) addPoly(svg, f.cls, f.pts);
|
||||
|
||||
for (const [face, transform] of TEXT_FACES) {
|
||||
const fo = svgEl('foreignObject', { width: CUBE, height: CUBE, transform });
|
||||
const div = document.createElementNS(XHTMLNS, 'div');
|
||||
div.setAttribute('class', `face-text ${face}`);
|
||||
fo.appendChild(div);
|
||||
svg.appendChild(fo);
|
||||
}
|
||||
const makeLogo = ({ fill, theme } = {}) => {
|
||||
const slot = document.createElement('div');
|
||||
slot.className = 'logo-slot';
|
||||
const svg = logoSvg.cloneNode(true);
|
||||
if (fill !== undefined) svg.style.setProperty('--fill', String(fill));
|
||||
if (theme) svg.setAttribute('data-theme', theme);
|
||||
slot.appendChild(svg);
|
||||
return slot;
|
||||
};
|
||||
|
||||
@@ -335,37 +278,30 @@
|
||||
return tile;
|
||||
};
|
||||
|
||||
// Row 1: cube on each background.
|
||||
const row = document.getElementById('row');
|
||||
row.appendChild(makeTile('bg-auto scheme-light', 'light', makeLogo()));
|
||||
row.appendChild(makeTile('bg-auto scheme-dark', 'dark', makeLogo()));
|
||||
row.appendChild(makeTile('bg-paper scheme-light', 'paper', makeLogo()));
|
||||
row.appendChild(makeTile('bg-auto scheme-light', 'light', makeLogo()));
|
||||
row.appendChild(makeTile('bg-auto scheme-dark', 'dark', makeLogo()));
|
||||
row.appendChild(makeTile('bg-paper scheme-light', 'paper', makeLogo()));
|
||||
row.appendChild(makeTile('bg-gradient scheme-light', 'gradient', makeLogo()));
|
||||
row.appendChild(makeTile('bg-image-1 scheme-light', 'img 1', makeLogo()));
|
||||
row.appendChild(makeTile('bg-image-2 scheme-light', 'img 2', makeLogo()));
|
||||
|
||||
// Row 2: fill levels.
|
||||
const fills = document.getElementById('fills');
|
||||
for (const f of [0, 0.1, 0.25, 0.5, 0.75, 1])
|
||||
fills.appendChild(makeTile('bg-auto scheme-light', `fill ${f}`, makeLogo({ fill: f })));
|
||||
|
||||
// Row 3: cube variants × bg theme (same matrix as demo.html).
|
||||
const variants = document.getElementById('variants');
|
||||
const neutLight = { faceColor: 'var(--border-color)', cubeScheme: 'light' };
|
||||
const neutDark = { faceColor: 'var(--border-color)', cubeScheme: 'dark' };
|
||||
const variantTiles = [
|
||||
{ label: 'light / light', bg: 'scheme-light', opts: neutLight },
|
||||
{ label: 'light / dark', bg: 'scheme-dark', opts: neutLight },
|
||||
{ label: 'dark / light', bg: 'scheme-light', opts: neutDark },
|
||||
{ label: 'dark / dark', bg: 'scheme-dark', opts: neutDark },
|
||||
{ label: 'light / light', bg: 'scheme-light', opts: { theme: 'light' } },
|
||||
{ label: 'light / dark', bg: 'scheme-dark', opts: { theme: 'light' } },
|
||||
{ label: 'dark / light', bg: 'scheme-light', opts: { theme: 'dark' } },
|
||||
{ label: 'dark / dark', bg: 'scheme-dark', opts: { theme: 'dark' } },
|
||||
{ label: 'orange / light', bg: 'scheme-light', opts: {} },
|
||||
{ label: 'orange / dark', bg: 'scheme-dark', opts: {} },
|
||||
];
|
||||
for (const v of variantTiles)
|
||||
variants.appendChild(makeTile(`bg-auto ${v.bg}`, v.label, makeLogo(v.opts)));
|
||||
|
||||
// Row 4: text-in-cube — same variant matrix as row 3 with realistic
|
||||
// block content (same as demo.html's `with-text` section).
|
||||
const sampleBlocks = [
|
||||
{ height: 912345, date: '2026-04-17', time: '12:00:00', miner: 'Foundry USA', mid: 12, min: 1, max: 64 },
|
||||
{ height: 912346, date: '2026-04-17', time: '12:10:23', miner: 'AntPool', mid: 8, min: 2, max: 21 },
|
||||
@@ -379,19 +315,16 @@
|
||||
const [, m, d] = iso.split('-').map(Number);
|
||||
return `${MONTHS[m - 1]} ${d}`;
|
||||
};
|
||||
// Tiny DOM builders.
|
||||
const el = (tag, cls, text) => {
|
||||
const e = document.createElement(tag);
|
||||
if (cls) e.className = cls;
|
||||
if (text != null) e.textContent = text;
|
||||
return e;
|
||||
};
|
||||
const p = (text, cls) => el('p', cls, text);
|
||||
const span = (text, cls) => el('span', cls, text);
|
||||
const slug = (s) => s.toLowerCase().replace(/\s+/g, '');
|
||||
const p = (text, cls) => el('p', cls, text);
|
||||
const span = (text, cls) => el('span', cls, text);
|
||||
const slug = (s) => s.toLowerCase().replace(/\s+/g, '');
|
||||
|
||||
// Mirror of website/scripts/explorer/render.js createHeightElement:
|
||||
// dimmed "#000…" prefix padding the height to 7 digits.
|
||||
const heightSpan = (h) => {
|
||||
const s = String(h);
|
||||
const prefix = span('#' + '0'.repeat(Math.max(0, 7 - s.length)), 'dim');
|
||||
@@ -403,37 +336,44 @@
|
||||
|
||||
const makeBlockCube = (opts, block) => {
|
||||
const slot = makeLogo(opts);
|
||||
const q = (sel) => slot.querySelector(sel);
|
||||
const [hh, mm] = block.time.slice(0, 5).split(':');
|
||||
const svg = slot.querySelector('svg');
|
||||
|
||||
// Top: date / HH:MM (colon dimmed).
|
||||
const faces = {};
|
||||
for (const [face, transform] of TEXT_FACES) {
|
||||
const fo = svgEl('foreignObject', { width: CUBE, height: CUBE, transform });
|
||||
const div = document.createElementNS(XHTMLNS, 'div');
|
||||
div.setAttribute('class', `face-text ${face}`);
|
||||
fo.appendChild(div);
|
||||
svg.appendChild(fo);
|
||||
faces[face] = div;
|
||||
}
|
||||
|
||||
const [hh, mm] = block.time.slice(0, 5).split(':');
|
||||
const timeP = p();
|
||||
timeP.append(hh, span(':', 'dim'), mm);
|
||||
q('.face-text.top').append(p(shortDate(block.date)), timeP);
|
||||
faces.top.append(p(shortDate(block.date)), timeP);
|
||||
|
||||
// Right: height (sm) / raw pool-logo + miner name (ellipsis-clipped).
|
||||
const heightP = p(null, 'height');
|
||||
heightP.appendChild(heightSpan(block.height));
|
||||
const poolDiv = el('div', 'pool');
|
||||
const logo = el('img', slug(block.miner));
|
||||
const nameSpan = span(block.miner.replace(/\s+(Pool|USA)$/i, '').trim());
|
||||
poolDiv.append(logo, nameSpan);
|
||||
q('.face-text.right').append(heightP, poolDiv);
|
||||
faces.right.append(heightP, poolDiv);
|
||||
|
||||
// Left: ~median / min-max / sat/vB (dash + unit dimmed).
|
||||
const range = p();
|
||||
range.append(String(block.min), span('-', 'dim'), String(block.max));
|
||||
const fees = el('div', 'fees');
|
||||
fees.append(p(`~${block.mid}`), range, p('sat/vB', 'dim'));
|
||||
q('.face-text.left').append(fees);
|
||||
faces.left.append(fees);
|
||||
|
||||
return slot;
|
||||
};
|
||||
|
||||
const withText = document.getElementById('with-text');
|
||||
variantTiles.forEach((v, i) =>
|
||||
withText.appendChild(makeTile(`bg-auto ${v.bg}`, v.label, makeBlockCube(v.opts, sampleBlocks[i])))
|
||||
);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
|
||||
<title>bitview</title>
|
||||
<style>
|
||||
.glass { fill-opacity: 0.4; }
|
||||
.glass-top, .liquid-top { fill: oklch(26.6% 0 0); }
|
||||
.glass-right, .liquid-right { fill: oklch(20% 0 0); }
|
||||
.glass-left, .liquid-left { fill: oklch(23.3% 0 0); }
|
||||
.glass-bottom { fill: oklch(10.1% 0 0); }
|
||||
.glass-rear-left { fill: oklch(26.6% 0 0); }
|
||||
.glass-rear-right { fill: oklch(23.3% 0 0); }
|
||||
.liquid {
|
||||
transform-box: view-box;
|
||||
transform: translateY(25%);
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
|
||||
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
|
||||
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
|
||||
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
|
||||
|
||||
<g clip-path="url(#hex)">
|
||||
<g class="liquid">
|
||||
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
|
||||
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
|
||||
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
|
||||
<title>bitview</title>
|
||||
<style>
|
||||
.glass { fill-opacity: 0.4; }
|
||||
.glass-top, .liquid-top { fill: oklch(90% 0 0); }
|
||||
.glass-right, .liquid-right { fill: oklch(83.4% 0 0); }
|
||||
.glass-left, .liquid-left { fill: oklch(86.7% 0 0); }
|
||||
.glass-bottom { fill: oklch(80.1% 0 0); }
|
||||
.glass-rear-left { fill: oklch(90% 0 0); }
|
||||
.glass-rear-right { fill: oklch(86.7% 0 0); }
|
||||
.liquid {
|
||||
transform-box: view-box;
|
||||
transform: translateY(25%);
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
|
||||
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
|
||||
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
|
||||
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
|
||||
|
||||
<g clip-path="url(#hex)">
|
||||
<g class="liquid">
|
||||
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
|
||||
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
|
||||
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
|
||||
<title>bitview</title>
|
||||
<style>
|
||||
.glass { fill-opacity: 0.4; }
|
||||
.glass-top, .liquid-top { fill: oklch(67.64% 0.191 44.41); }
|
||||
.glass-right, .liquid-right { fill: oklch(61.04% 0.191 44.41); }
|
||||
.glass-left, .liquid-left { fill: oklch(64.34% 0.191 44.41); }
|
||||
.glass-bottom { fill: oklch(57.74% 0.191 44.41); }
|
||||
.glass-rear-left { fill: oklch(67.64% 0.191 44.41); }
|
||||
.glass-rear-right { fill: oklch(64.34% 0.191 44.41); }
|
||||
.liquid {
|
||||
transform-box: view-box;
|
||||
transform: translateY(25%);
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
|
||||
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
|
||||
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
|
||||
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
|
||||
|
||||
<g clip-path="url(#hex)">
|
||||
<g class="liquid">
|
||||
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
|
||||
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
|
||||
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,57 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 173.2 200" role="img" aria-label="bitview">
|
||||
<title>bitview</title>
|
||||
<style>
|
||||
svg {
|
||||
--fill: 0.5;
|
||||
--face-top: oklch(67.64% 0.191 44.41);
|
||||
--face-left: oklch(64.34% 0.191 44.41);
|
||||
--face-right: oklch(61.04% 0.191 44.41);
|
||||
--face-bottom: oklch(57.74% 0.191 44.41);
|
||||
}
|
||||
svg[data-theme="light"] {
|
||||
--face-top: oklch(90% 0 0);
|
||||
--face-left: oklch(86.7% 0 0);
|
||||
--face-right: oklch(83.4% 0 0);
|
||||
--face-bottom: oklch(80.1% 0 0);
|
||||
}
|
||||
svg[data-theme="dark"] {
|
||||
--face-top: oklch(26.6% 0 0);
|
||||
--face-left: oklch(23.3% 0 0);
|
||||
--face-right: oklch(20% 0 0);
|
||||
--face-bottom: oklch(10.1% 0 0);
|
||||
}
|
||||
.glass { fill-opacity: 0.4; }
|
||||
.glass-top, .liquid-top { fill: var(--face-top); }
|
||||
.glass-right, .liquid-right { fill: var(--face-right); }
|
||||
.glass-left, .liquid-left { fill: var(--face-left); }
|
||||
.glass-bottom { fill: var(--face-bottom); }
|
||||
.glass-rear-left { fill: var(--face-top); }
|
||||
.glass-rear-right { fill: var(--face-left); }
|
||||
.liquid {
|
||||
transform-box: view-box;
|
||||
transform: translateY(calc((1 - var(--fill)) * 50%));
|
||||
opacity: ceil(var(--fill));
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<clipPath id="hex" clipPathUnits="userSpaceOnUse">
|
||||
<polygon points="86.6,0 173.2,50 173.2,150 86.6,200 0,150 0,50"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<polygon class="glass glass-bottom" points="86.6,100 173.2,150 86.6,200 0,150"/>
|
||||
<polygon class="glass glass-rear-left" points="86.6,100 0,150 0,50 86.6,0"/>
|
||||
<polygon class="glass glass-rear-right" points="86.6,100 173.2,150 173.2,50 86.6,0"/>
|
||||
|
||||
<g clip-path="url(#hex)">
|
||||
<g class="liquid">
|
||||
<polygon class="liquid-top" points="86.6,0 173.2,50 86.6,100 0,50"/>
|
||||
<polygon class="liquid-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="liquid-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<polygon class="glass glass-top" points="0,50 86.6,0 173.2,50 86.6,100"/>
|
||||
<polygon class="glass glass-right" points="173.2,50 173.2,150 86.6,200 86.6,100"/>
|
||||
<polygon class="glass glass-left" points="0,50 0,150 86.6,200 86.6,100"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 562 B |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -96,13 +96,11 @@
|
||||
</script>
|
||||
<script type="module" src="/scripts/entry.js"></script>
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="196x196"
|
||||
href="/assets/favicon-196.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/assets/apple-icon-180.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png" />
|
||||
<link rel="shortcut icon" href="/assets/favicon/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="bitview" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -16,25 +16,25 @@
|
||||
"lang": "en",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/manifest-icon-192.maskable.png",
|
||||
"src": "/assets/favicon/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-192.maskable.png",
|
||||
"src": "/assets/favicon/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-512.maskable.png",
|
||||
"src": "/assets/favicon/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-512.maskable.png",
|
||||
"src": "/assets/favicon/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
* @typedef {Brk.AddrUtxoPattern} AvgAmountPattern
|
||||
* @typedef {Brk.SeriesTree_Addrs_Exposed} ExposedTree
|
||||
* @typedef {Brk.SeriesTree_Addrs_Reused} ReusedTree
|
||||
* @typedef {Brk.SeriesTree_Addrs_Respent} RespentTree
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -178,6 +178,7 @@ export function buildCohortData() {
|
||||
avgAmount: addrs.avgAmount[key],
|
||||
exposed: addrs.exposed,
|
||||
reused: addrs.reused,
|
||||
respent: addrs.respent,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ export function createCohortFolderAddress(cohort) {
|
||||
createProfitabilitySectionWithProfitLoss({ cohort, title }),
|
||||
createActivitySectionMinimal({ cohort, title }),
|
||||
avgHoldingsSubtree(cohort.avgAmount, title),
|
||||
reusedSubtree(cohort.reused, cohort.key, title),
|
||||
reusedSubtree(cohort.reused, cohort.respent, cohort.key, title),
|
||||
exposedSubtree(cohort.exposed, cohort.key, title),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -103,154 +103,254 @@ export function createNetworkSection() {
|
||||
{ key: "total", name: "Total", color: colors.default },
|
||||
]);
|
||||
|
||||
const reusedSetEntries =
|
||||
/**
|
||||
* @param {AddressableType | "all"} key
|
||||
* @param {(name: string) => string} title
|
||||
*/
|
||||
(key, title) => [
|
||||
{
|
||||
name: "Compare",
|
||||
title: title("Reused Address Count"),
|
||||
bottom: [
|
||||
line({
|
||||
series: addrs.reused.count.funded[key],
|
||||
name: "Funded",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
series: addrs.reused.count.total[key],
|
||||
name: "Total",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Funded",
|
||||
title: title("Funded Reused Addresses"),
|
||||
bottom: [
|
||||
line({
|
||||
series: addrs.reused.count.funded[key],
|
||||
name: "Funded Reused",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Total Reused Addresses"),
|
||||
bottom: [
|
||||
line({
|
||||
series: addrs.reused.count.total[key],
|
||||
name: "Total Reused",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const reusedInputsSubtree =
|
||||
/**
|
||||
* @param {AddressableType | "all"} key
|
||||
* @param {(name: string) => string} title
|
||||
*/
|
||||
(key, title) => ({
|
||||
name: "Inputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: chartsFromCount({
|
||||
pattern: addrs.reused.events.inputFromReusedAddrCount[key],
|
||||
title,
|
||||
metric: "Transaction Inputs from Reused Addresses",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: chartsFromPercentCumulative({
|
||||
pattern: addrs.reused.events.inputFromReusedAddrShare[key],
|
||||
title,
|
||||
metric: "Share of Transaction Inputs from Reused Addresses",
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const reusedOutputsSubtreeForAll =
|
||||
/** @param {(name: string) => string} title */
|
||||
(title) => ({
|
||||
name: "Outputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: chartsFromCount({
|
||||
pattern: addrs.reused.events.outputToReusedAddrCount.all,
|
||||
title,
|
||||
metric: "Transaction Outputs to Reused Addresses",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: chartsFromPercentCumulativeEntries({
|
||||
entries: [
|
||||
{
|
||||
name: "All",
|
||||
pattern: addrs.reused.events.outputToReusedAddrShare.all,
|
||||
},
|
||||
{
|
||||
name: "Spendable",
|
||||
pattern: addrs.reused.events.spendableOutputToReusedAddrShare,
|
||||
color: colors.gray,
|
||||
},
|
||||
],
|
||||
title,
|
||||
metric: "Share of Transaction Outputs to Reused Addresses",
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const reusedActiveSubtreeForAll =
|
||||
/** @param {(name: string) => string} title */
|
||||
(title) => ({
|
||||
name: "Active",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: averagesArray({
|
||||
windows: addrs.reused.events.activeReusedAddrCount,
|
||||
title,
|
||||
metric: "Active Reused Addresses",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: averagesArray({
|
||||
windows: addrs.reused.events.activeReusedAddrShare,
|
||||
title,
|
||||
metric: "Active Reused Address Share",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const reusedSubtreeForAll =
|
||||
/** @param {(name: string) => string} title */
|
||||
(title) => ({
|
||||
name: "Reused",
|
||||
tree: [
|
||||
...reusedSetEntries("all", title),
|
||||
reusedActiveSubtreeForAll(title),
|
||||
reusedOutputsSubtreeForAll(title),
|
||||
reusedInputsSubtree("all", title),
|
||||
],
|
||||
});
|
||||
(title) => {
|
||||
const reused = addrs.reused;
|
||||
const respent = addrs.respent;
|
||||
const key = /** @type {const} */ ("all");
|
||||
|
||||
/**
|
||||
* Windowed sums + cumulative, overlaying reused (primary) and respent (gray).
|
||||
* @param {CountPattern<number>} reusedPattern
|
||||
* @param {CountPattern<number>} respentPattern
|
||||
* @param {string} metric
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
const countPair = (reusedPattern, respentPattern, metric) => [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${metric}`),
|
||||
bottom: [
|
||||
line({
|
||||
series: reusedPattern.sum[w.key],
|
||||
name: "2+ Funded",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
series: respentPattern.sum[w.key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: title(`Cumulative ${metric}`),
|
||||
bottom: [
|
||||
line({
|
||||
series: reusedPattern.cumulative,
|
||||
name: "2+ Funded",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
series: respentPattern.cumulative,
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
name: "Reused",
|
||||
tree: [
|
||||
{
|
||||
name: "Funded",
|
||||
title: title("Funded Reused Addresses"),
|
||||
bottom: [
|
||||
line({
|
||||
series: reused.count.funded[key],
|
||||
name: "2+ Funded",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
series: respent.count.funded[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Total Reused Addresses"),
|
||||
bottom: [
|
||||
line({
|
||||
series: reused.count.total[key],
|
||||
name: "2+ Funded",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
series: respent.count.total[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Active",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Active Reused Addresses`),
|
||||
bottom: [
|
||||
line({
|
||||
series: reused.events.activeReusedAddrCount[w.key],
|
||||
name: "2+ Funded",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
line({
|
||||
series: respent.events.activeReusedAddrCount[w.key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} Active Reused Address Share`),
|
||||
bottom: [
|
||||
line({
|
||||
series: reused.events.activeReusedAddrShare[w.key],
|
||||
name: "2+ Funded",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
series: respent.events.activeReusedAddrShare[w.key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Outputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: countPair(
|
||||
reused.events.outputToReusedAddrCount[key],
|
||||
respent.events.outputToReusedAddrCount[key],
|
||||
"Transaction Outputs to Reused Addresses",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: [
|
||||
{
|
||||
name: "All",
|
||||
tree: chartsFromPercentCumulativeEntries({
|
||||
entries: [
|
||||
{
|
||||
name: "2+ Funded",
|
||||
pattern: reused.events.outputToReusedAddrShare[key],
|
||||
},
|
||||
{
|
||||
name: "2+ Spent",
|
||||
pattern: respent.events.outputToReusedAddrShare[key],
|
||||
color: colors.gray,
|
||||
},
|
||||
],
|
||||
title,
|
||||
metric: "Share of Transaction Outputs to Reused Addresses",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Spendable",
|
||||
tree: chartsFromPercentCumulativeEntries({
|
||||
entries: [
|
||||
{
|
||||
name: "2+ Funded",
|
||||
pattern: reused.events.spendableOutputToReusedAddrShare,
|
||||
},
|
||||
{
|
||||
name: "2+ Spent",
|
||||
pattern: respent.events.spendableOutputToReusedAddrShare,
|
||||
color: colors.gray,
|
||||
},
|
||||
],
|
||||
title,
|
||||
metric: "Share of Spendable Transaction Outputs to Reused Addresses",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Inputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: countPair(
|
||||
reused.events.inputFromReusedAddrCount[key],
|
||||
respent.events.inputFromReusedAddrCount[key],
|
||||
"Transaction Inputs from Reused Addresses",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: chartsFromPercentCumulativeEntries({
|
||||
entries: [
|
||||
{
|
||||
name: "2+ Funded",
|
||||
pattern: reused.events.inputFromReusedAddrShare[key],
|
||||
},
|
||||
{
|
||||
name: "2+ Spent",
|
||||
pattern: respent.events.inputFromReusedAddrShare[key],
|
||||
color: colors.gray,
|
||||
},
|
||||
],
|
||||
title,
|
||||
metric: "Share of Transaction Inputs from Reused Addresses",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Supply",
|
||||
title: title("Supply in Reused Addresses"),
|
||||
bottom: [
|
||||
...satsBtcUsd({ pattern: reused.supply[key], name: "2+ Funded" }),
|
||||
...satsBtcUsd({
|
||||
pattern: respent.supply[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
title: title("Share of Supply in Reused Addresses"),
|
||||
bottom: [
|
||||
...percentRatio({
|
||||
pattern: reused.supply.share[key],
|
||||
name: "2+ Funded",
|
||||
}),
|
||||
...percentRatio({
|
||||
pattern: respent.supply.share[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const countSubtree =
|
||||
@@ -328,6 +428,12 @@ export function createNetworkSection() {
|
||||
name: "Reused Share",
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
series: addrs.respent.events.activeReusedAddrShare[w.key],
|
||||
name: "Respent Share",
|
||||
color: colors.gray,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
},
|
||||
@@ -413,7 +519,7 @@ export function createNetworkSection() {
|
||||
unit: Unit.count,
|
||||
}),
|
||||
activitySubtreeForType(addrType, title),
|
||||
reusedSubtree(addrs.reused, addrType, title),
|
||||
reusedSubtree(addrs.reused, addrs.respent, addrType, title),
|
||||
exposedSubtree(addrs.exposed, addrType, title),
|
||||
avgHoldingsSubtree(addrs.avgAmount[addrType], title),
|
||||
];
|
||||
@@ -458,6 +564,113 @@ export function createNetworkSection() {
|
||||
}),
|
||||
);
|
||||
|
||||
const reuseCompareByTypeFolder =
|
||||
/**
|
||||
* @param {string} label - "Reused" or "Respent"
|
||||
* @param {ReusedTree | RespentTree} tree
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
(label, tree) => ({
|
||||
name: label,
|
||||
tree: [
|
||||
{
|
||||
name: "Funded",
|
||||
title: `Funded ${label} Address Count by Type`,
|
||||
bottom: typeLines((t) => tree.count.funded[t.key]),
|
||||
},
|
||||
{
|
||||
name: "Total",
|
||||
title: `Total ${label} Address Count by Type`,
|
||||
bottom: typeLines((t) => tree.count.total[t.key]),
|
||||
},
|
||||
{
|
||||
name: "Outputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: groupedWindowsCumulative({
|
||||
list: addressTypes,
|
||||
title: (s) => s,
|
||||
metricTitle: `Transaction Outputs to ${label} Addresses by Type`,
|
||||
getWindowSeries: (t, key) =>
|
||||
tree.events.outputToReusedAddrCount[t.key].sum[key],
|
||||
getCumulativeSeries: (t) =>
|
||||
tree.events.outputToReusedAddrCount[t.key].cumulative,
|
||||
seriesFn: line,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: `${w.title} Share of Transaction Outputs to ${label} Addresses by Type`,
|
||||
bottom: typeLines(
|
||||
(t) =>
|
||||
tree.events.outputToReusedAddrShare[t.key][w.key]
|
||||
.percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: `Cumulative Share of Transaction Outputs to ${label} Addresses by Type`,
|
||||
bottom: typeLines(
|
||||
(t) =>
|
||||
tree.events.outputToReusedAddrShare[t.key].percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Inputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: groupedWindowsCumulative({
|
||||
list: addressTypes,
|
||||
title: (s) => s,
|
||||
metricTitle: `Transaction Inputs from ${label} Addresses by Type`,
|
||||
getWindowSeries: (t, key) =>
|
||||
tree.events.inputFromReusedAddrCount[t.key].sum[key],
|
||||
getCumulativeSeries: (t) =>
|
||||
tree.events.inputFromReusedAddrCount[t.key].cumulative,
|
||||
seriesFn: line,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: `${w.title} Share of Transaction Inputs from ${label} Addresses by Type`,
|
||||
bottom: typeLines(
|
||||
(t) =>
|
||||
tree.events.inputFromReusedAddrShare[t.key][w.key]
|
||||
.percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: `Cumulative Share of Transaction Inputs from ${label} Addresses by Type`,
|
||||
bottom: typeLines(
|
||||
(t) => tree.events.inputFromReusedAddrShare[t.key].percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
name: "Compare",
|
||||
tree: [
|
||||
@@ -525,120 +738,12 @@ export function createNetworkSection() {
|
||||
})),
|
||||
},
|
||||
|
||||
// Reused
|
||||
// Address Reuse: receive-side (Reused) + spend-side (Respent)
|
||||
{
|
||||
name: "Reused",
|
||||
name: "Address Reuse",
|
||||
tree: [
|
||||
{
|
||||
name: "Funded",
|
||||
title: "Funded Reused Address Count by Type",
|
||||
bottom: typeLines((t) => addrs.reused.count.funded[t.key]),
|
||||
},
|
||||
{
|
||||
name: "Total",
|
||||
title: "Total Reused Address Count by Type",
|
||||
bottom: typeLines((t) => addrs.reused.count.total[t.key]),
|
||||
},
|
||||
{
|
||||
name: "Outputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: groupedWindowsCumulative({
|
||||
list: addressTypes,
|
||||
title: (s) => s,
|
||||
metricTitle:
|
||||
"Transaction Outputs to Reused Addresses by Type",
|
||||
getWindowSeries: (t, key) =>
|
||||
addrs.reused.events.outputToReusedAddrCount[t.key].sum[
|
||||
key
|
||||
],
|
||||
getCumulativeSeries: (t) =>
|
||||
addrs.reused.events.outputToReusedAddrCount[t.key]
|
||||
.cumulative,
|
||||
seriesFn: line,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: `${w.title} Share of Transaction Outputs to Reused Addresses by Type`,
|
||||
bottom: typeLines(
|
||||
(t) =>
|
||||
addrs.reused.events.outputToReusedAddrShare[t.key][
|
||||
w.key
|
||||
].percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title:
|
||||
"Cumulative Share of Transaction Outputs to Reused Addresses by Type",
|
||||
bottom: typeLines(
|
||||
(t) =>
|
||||
addrs.reused.events.outputToReusedAddrShare[t.key]
|
||||
.percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Inputs",
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: groupedWindowsCumulative({
|
||||
list: addressTypes,
|
||||
title: (s) => s,
|
||||
metricTitle:
|
||||
"Transaction Inputs from Reused Addresses by Type",
|
||||
getWindowSeries: (t, key) =>
|
||||
addrs.reused.events.inputFromReusedAddrCount[t.key].sum[
|
||||
key
|
||||
],
|
||||
getCumulativeSeries: (t) =>
|
||||
addrs.reused.events.inputFromReusedAddrCount[t.key]
|
||||
.cumulative,
|
||||
seriesFn: line,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: `${w.title} Share of Transaction Inputs from Reused Addresses by Type`,
|
||||
bottom: typeLines(
|
||||
(t) =>
|
||||
addrs.reused.events.inputFromReusedAddrShare[t.key][
|
||||
w.key
|
||||
].percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title:
|
||||
"Cumulative Share of Transaction Inputs from Reused Addresses by Type",
|
||||
bottom: typeLines(
|
||||
(t) =>
|
||||
addrs.reused.events.inputFromReusedAddrShare[t.key]
|
||||
.percent,
|
||||
Unit.percentage,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
reuseCompareByTypeFolder("Reused", addrs.reused),
|
||||
reuseCompareByTypeFolder("Respent", addrs.respent),
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
baseline,
|
||||
price,
|
||||
percentRatio,
|
||||
chartsFromCount,
|
||||
chartsFromPercentCumulative,
|
||||
chartsFromPercentCumulativeEntries,
|
||||
sumsAndAveragesCumulativeWith,
|
||||
} from "./series.js";
|
||||
import { priceLine, priceLines } from "./constants.js";
|
||||
@@ -365,45 +364,78 @@ export function exposedSubtree(exposed, key, title) {
|
||||
}
|
||||
|
||||
/**
|
||||
* "Reused" subtree (per-type / per-cohort — no "Active" window, since that
|
||||
* data is only tracked globally). Shape:
|
||||
* Compare (funded + total) / Funded / Total / Outputs / Inputs.
|
||||
* "Reused" subtree (per-type / per-cohort, no "Active" window since that
|
||||
* data is only tracked globally). Respent (addresses whose outputs have
|
||||
* been spent more than once) is a subset of reused, so each chart layers
|
||||
* both series in two colors: reused in the primary color, respent in
|
||||
* gray. Shape: Funded / Total / Outputs / Inputs / Supply / Share.
|
||||
* @param {ReusedTree} reused
|
||||
* @param {RespentTree} respent
|
||||
* @param {AddressableType | "all"} key
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function reusedSubtree(reused, key, title) {
|
||||
export function reusedSubtree(reused, respent, key, title) {
|
||||
/**
|
||||
* Windowed sums + cumulative, overlaying reused (primary) and respent (gray).
|
||||
* @param {CountPattern<number>} reusedPattern
|
||||
* @param {CountPattern<number>} respentPattern
|
||||
* @param {string} metric
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
const countPair = (reusedPattern, respentPattern, metric) => [
|
||||
...ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`${w.title} ${metric}`),
|
||||
bottom: [
|
||||
line({ series: reusedPattern.sum[w.key], name: "2+ Funded", unit: Unit.count }),
|
||||
line({
|
||||
series: respentPattern.sum[w.key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
})),
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: title(`Cumulative ${metric}`),
|
||||
bottom: [
|
||||
line({ series: reusedPattern.cumulative, name: "2+ Funded", unit: Unit.count }),
|
||||
line({
|
||||
series: respentPattern.cumulative,
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
name: "Reused",
|
||||
tree: [
|
||||
{
|
||||
name: "Compare",
|
||||
title: title("Reused Address Count"),
|
||||
name: "Funded",
|
||||
title: title("Funded Reused Addresses"),
|
||||
bottom: [
|
||||
line({ series: reused.count.funded[key], name: "Funded", unit: Unit.count }),
|
||||
line({ series: reused.count.funded[key], name: "2+ Funded", unit: Unit.count }),
|
||||
line({
|
||||
series: reused.count.total[key],
|
||||
name: "Total",
|
||||
series: respent.count.funded[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Funded",
|
||||
title: title("Funded Reused Addresses"),
|
||||
bottom: [
|
||||
line({ series: reused.count.funded[key], name: "Funded Reused", unit: Unit.count }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Total Reused Addresses"),
|
||||
bottom: [
|
||||
line({ series: reused.count.total[key], name: "2+ Funded", unit: Unit.count }),
|
||||
line({
|
||||
series: reused.count.total[key],
|
||||
name: "Total Reused",
|
||||
series: respent.count.total[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
unit: Unit.count,
|
||||
}),
|
||||
@@ -414,17 +446,26 @@ export function reusedSubtree(reused, key, title) {
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: chartsFromCount({
|
||||
pattern: reused.events.outputToReusedAddrCount[key],
|
||||
title,
|
||||
metric: "Transaction Outputs to Reused Addresses",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
tree: countPair(
|
||||
reused.events.outputToReusedAddrCount[key],
|
||||
respent.events.outputToReusedAddrCount[key],
|
||||
"Transaction Outputs to Reused Addresses",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: chartsFromPercentCumulative({
|
||||
pattern: reused.events.outputToReusedAddrShare[key],
|
||||
tree: chartsFromPercentCumulativeEntries({
|
||||
entries: [
|
||||
{
|
||||
name: "2+ Funded",
|
||||
pattern: reused.events.outputToReusedAddrShare[key],
|
||||
},
|
||||
{
|
||||
name: "2+ Spent",
|
||||
pattern: respent.events.outputToReusedAddrShare[key],
|
||||
color: colors.gray,
|
||||
},
|
||||
],
|
||||
title,
|
||||
metric: "Share of Transaction Outputs to Reused Addresses",
|
||||
}),
|
||||
@@ -436,23 +477,56 @@ export function reusedSubtree(reused, key, title) {
|
||||
tree: [
|
||||
{
|
||||
name: "Count",
|
||||
tree: chartsFromCount({
|
||||
pattern: reused.events.inputFromReusedAddrCount[key],
|
||||
title,
|
||||
metric: "Transaction Inputs from Reused Addresses",
|
||||
unit: Unit.count,
|
||||
}),
|
||||
tree: countPair(
|
||||
reused.events.inputFromReusedAddrCount[key],
|
||||
respent.events.inputFromReusedAddrCount[key],
|
||||
"Transaction Inputs from Reused Addresses",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
tree: chartsFromPercentCumulative({
|
||||
pattern: reused.events.inputFromReusedAddrShare[key],
|
||||
tree: chartsFromPercentCumulativeEntries({
|
||||
entries: [
|
||||
{
|
||||
name: "2+ Funded",
|
||||
pattern: reused.events.inputFromReusedAddrShare[key],
|
||||
},
|
||||
{
|
||||
name: "2+ Spent",
|
||||
pattern: respent.events.inputFromReusedAddrShare[key],
|
||||
color: colors.gray,
|
||||
},
|
||||
],
|
||||
title,
|
||||
metric: "Share of Transaction Inputs from Reused Addresses",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Supply",
|
||||
title: title("Supply in Reused Addresses"),
|
||||
bottom: [
|
||||
...satsBtcUsd({ pattern: reused.supply[key], name: "2+ Funded" }),
|
||||
...satsBtcUsd({
|
||||
pattern: respent.supply[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
title: title("Share of Supply in Reused Addresses"),
|
||||
bottom: [
|
||||
...percentRatio({ pattern: reused.supply.share[key], name: "2+ Funded" }),
|
||||
...percentRatio({
|
||||
pattern: respent.supply.share[key],
|
||||
name: "2+ Spent",
|
||||
color: colors.gray,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
* ============================================================================
|
||||
*
|
||||
* Addressable cohort with address count (for "type" cohorts - uses OutputsRealizedSupplyUnrealizedPattern2)
|
||||
* @typedef {{ name: string, key: AddressableType, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern, avgAmount: AvgAmountPattern, exposed: ExposedTree, reused: ReusedTree }} CohortAddr
|
||||
* @typedef {{ name: string, key: AddressableType, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern, avgAmount: AvgAmountPattern, exposed: ExposedTree, reused: ReusedTree, respent: RespentTree }} CohortAddr
|
||||
*
|
||||
* ============================================================================
|
||||
* Cohort Group Types (by capability)
|
||||
|
||||