mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-23 22:29:59 -07:00
global: snap
This commit is contained in:
48
Cargo.lock
generated
48
Cargo.lock
generated
@@ -834,9 +834,9 @@ checksum = "1c53ba0f290bfc610084c05582d9c5d421662128fc69f4bf236707af6fd321b9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.59"
|
||||
version = "1.2.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -1602,6 +1602,12 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -1888,12 +1894,12 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.1"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown 0.17.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1965,9 +1971,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.94"
|
||||
version = "0.3.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -2043,9 +2049,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -2726,9 +2732,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.10"
|
||||
version = "0.103.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -3504,9 +3510,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.117"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -3517,9 +3523,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.117"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -3527,9 +3533,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.117"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -3540,9 +3546,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.117"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3583,9 +3589,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.94"
|
||||
version = "0.3.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
|
||||
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
||||
@@ -69,7 +69,7 @@ corepc-client = { package = "brk-corepc-client", version = "0.11.0", features =
|
||||
corepc-jsonrpc = { package = "brk-corepc-jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "=3.0.4"
|
||||
indexmap = { version = "2.13.1", features = ["serde"] }
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.23", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
parking_lot = "0.12.5"
|
||||
|
||||
@@ -88,13 +88,14 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
let fetch_call = if endpoint.returns_json() {
|
||||
"this.getJson(path, { signal, onUpdate })"
|
||||
} else {
|
||||
"this.getText(path, { signal })"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(
|
||||
output,
|
||||
" return this.getJson(`{}`, {{ signal, onUpdate }});",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " const path = `{}`;", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " const params = new URLSearchParams();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
@@ -122,25 +123,13 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if (format === 'csv') {{").unwrap();
|
||||
writeln!(output, " return this.getText(path, {{ signal }});").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return this.getJson(path, {{ signal, onUpdate }});"
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" return this.getJson(path, {{ signal, onUpdate }});"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if (format === 'csv') return this.getText(path, {{ signal }});").unwrap();
|
||||
}
|
||||
writeln!(output, " return {};", fetch_call).unwrap();
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4799,7 +4799,6 @@ impl SeriesTree_Indexes_Addr_OpReturn {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Height {
|
||||
pub identity: SeriesPattern18<Height>,
|
||||
pub minute10: SeriesPattern18<Minute10>,
|
||||
pub minute30: SeriesPattern18<Minute30>,
|
||||
pub hour1: SeriesPattern18<Hour1>,
|
||||
@@ -4821,7 +4820,6 @@ pub struct SeriesTree_Indexes_Height {
|
||||
impl SeriesTree_Indexes_Height {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern18::new(client.clone(), "height".to_string()),
|
||||
minute10: SeriesPattern18::new(client.clone(), "minute10".to_string()),
|
||||
minute30: SeriesPattern18::new(client.clone(), "minute30".to_string()),
|
||||
hour1: SeriesPattern18::new(client.clone(), "hour1".to_string()),
|
||||
@@ -4844,31 +4842,25 @@ impl SeriesTree_Indexes_Height {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Epoch {
|
||||
pub identity: SeriesPattern17<Epoch>,
|
||||
pub first_height: SeriesPattern17<Height>,
|
||||
pub height_count: SeriesPattern17<StoredU64>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Epoch {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern17::new(client.clone(), "epoch".to_string()),
|
||||
first_height: SeriesPattern17::new(client.clone(), "first_height".to_string()),
|
||||
height_count: SeriesPattern17::new(client.clone(), "height_count".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Halving {
|
||||
pub identity: SeriesPattern16<Halving>,
|
||||
pub first_height: SeriesPattern16<Height>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Halving {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern16::new(client.clone(), "halving".to_string()),
|
||||
first_height: SeriesPattern16::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4876,14 +4868,12 @@ impl SeriesTree_Indexes_Halving {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Minute10 {
|
||||
pub identity: SeriesPattern3<Minute10>,
|
||||
pub first_height: SeriesPattern3<Height>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Minute10 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern3::new(client.clone(), "minute10_index".to_string()),
|
||||
first_height: SeriesPattern3::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4891,14 +4881,12 @@ impl SeriesTree_Indexes_Minute10 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Minute30 {
|
||||
pub identity: SeriesPattern4<Minute30>,
|
||||
pub first_height: SeriesPattern4<Height>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Minute30 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern4::new(client.clone(), "minute30_index".to_string()),
|
||||
first_height: SeriesPattern4::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4906,14 +4894,12 @@ impl SeriesTree_Indexes_Minute30 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Hour1 {
|
||||
pub identity: SeriesPattern5<Hour1>,
|
||||
pub first_height: SeriesPattern5<Height>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Hour1 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern5::new(client.clone(), "hour1_index".to_string()),
|
||||
first_height: SeriesPattern5::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4921,14 +4907,12 @@ impl SeriesTree_Indexes_Hour1 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Hour4 {
|
||||
pub identity: SeriesPattern6<Hour4>,
|
||||
pub first_height: SeriesPattern6<Height>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Hour4 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern6::new(client.clone(), "hour4_index".to_string()),
|
||||
first_height: SeriesPattern6::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4936,14 +4920,12 @@ impl SeriesTree_Indexes_Hour4 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Hour12 {
|
||||
pub identity: SeriesPattern7<Hour12>,
|
||||
pub first_height: SeriesPattern7<Height>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Hour12 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern7::new(client.clone(), "hour12_index".to_string()),
|
||||
first_height: SeriesPattern7::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4951,33 +4933,29 @@ impl SeriesTree_Indexes_Hour12 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Day1 {
|
||||
pub identity: SeriesPattern8<Day1>,
|
||||
pub date: SeriesPattern8<Date>,
|
||||
pub first_height: SeriesPattern8<Height>,
|
||||
pub height_count: SeriesPattern8<StoredU64>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Day1 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern8::new(client.clone(), "day1_index".to_string()),
|
||||
date: SeriesPattern8::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern8::new(client.clone(), "first_height".to_string()),
|
||||
height_count: SeriesPattern8::new(client.clone(), "height_count".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Day3 {
|
||||
pub identity: SeriesPattern9<Day3>,
|
||||
pub date: SeriesPattern9<Date>,
|
||||
pub first_height: SeriesPattern9<Height>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Indexes_Day3 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern9::new(client.clone(), "day3_index".to_string()),
|
||||
date: SeriesPattern9::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern9::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4985,7 +4963,6 @@ impl SeriesTree_Indexes_Day3 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Week1 {
|
||||
pub identity: SeriesPattern10<Week1>,
|
||||
pub date: SeriesPattern10<Date>,
|
||||
pub first_height: SeriesPattern10<Height>,
|
||||
}
|
||||
@@ -4993,7 +4970,6 @@ pub struct SeriesTree_Indexes_Week1 {
|
||||
impl SeriesTree_Indexes_Week1 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern10::new(client.clone(), "week1_index".to_string()),
|
||||
date: SeriesPattern10::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern10::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
@@ -5002,7 +4978,6 @@ impl SeriesTree_Indexes_Week1 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Month1 {
|
||||
pub identity: SeriesPattern11<Month1>,
|
||||
pub date: SeriesPattern11<Date>,
|
||||
pub first_height: SeriesPattern11<Height>,
|
||||
}
|
||||
@@ -5010,7 +4985,6 @@ pub struct SeriesTree_Indexes_Month1 {
|
||||
impl SeriesTree_Indexes_Month1 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern11::new(client.clone(), "month1_index".to_string()),
|
||||
date: SeriesPattern11::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern11::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
@@ -5019,7 +4993,6 @@ impl SeriesTree_Indexes_Month1 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Month3 {
|
||||
pub identity: SeriesPattern12<Month3>,
|
||||
pub date: SeriesPattern12<Date>,
|
||||
pub first_height: SeriesPattern12<Height>,
|
||||
}
|
||||
@@ -5027,7 +5000,6 @@ pub struct SeriesTree_Indexes_Month3 {
|
||||
impl SeriesTree_Indexes_Month3 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern12::new(client.clone(), "month3_index".to_string()),
|
||||
date: SeriesPattern12::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern12::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
@@ -5036,7 +5008,6 @@ impl SeriesTree_Indexes_Month3 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Month6 {
|
||||
pub identity: SeriesPattern13<Month6>,
|
||||
pub date: SeriesPattern13<Date>,
|
||||
pub first_height: SeriesPattern13<Height>,
|
||||
}
|
||||
@@ -5044,7 +5015,6 @@ pub struct SeriesTree_Indexes_Month6 {
|
||||
impl SeriesTree_Indexes_Month6 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern13::new(client.clone(), "month6_index".to_string()),
|
||||
date: SeriesPattern13::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern13::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
@@ -5053,7 +5023,6 @@ impl SeriesTree_Indexes_Month6 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Year1 {
|
||||
pub identity: SeriesPattern14<Year1>,
|
||||
pub date: SeriesPattern14<Date>,
|
||||
pub first_height: SeriesPattern14<Height>,
|
||||
}
|
||||
@@ -5061,7 +5030,6 @@ pub struct SeriesTree_Indexes_Year1 {
|
||||
impl SeriesTree_Indexes_Year1 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern14::new(client.clone(), "year1_index".to_string()),
|
||||
date: SeriesPattern14::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern14::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
@@ -5070,7 +5038,6 @@ impl SeriesTree_Indexes_Year1 {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Indexes_Year10 {
|
||||
pub identity: SeriesPattern15<Year10>,
|
||||
pub date: SeriesPattern15<Date>,
|
||||
pub first_height: SeriesPattern15<Height>,
|
||||
}
|
||||
@@ -5078,7 +5045,6 @@ pub struct SeriesTree_Indexes_Year10 {
|
||||
impl SeriesTree_Indexes_Year10 {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
identity: SeriesPattern15::new(client.clone(), "year10_index".to_string()),
|
||||
date: SeriesPattern15::new(client.clone(), "date".to_string()),
|
||||
first_height: SeriesPattern15::new(client.clone(), "first_height".to_string()),
|
||||
}
|
||||
@@ -8712,6 +8678,15 @@ impl BrkClient {
|
||||
self.base.get_json(&format!("/api/server/sync"))
|
||||
}
|
||||
|
||||
/// Txid by index
|
||||
///
|
||||
/// Retrieve the transaction ID (txid) at a given global transaction index. Returns the txid as plain text.
|
||||
///
|
||||
/// Endpoint: `GET /api/tx-index/{index}`
|
||||
pub fn get_tx_by_index(&self, index: TxIndex) -> Result<String> {
|
||||
self.base.get_text(&format!("/api/tx-index/{index}"))
|
||||
}
|
||||
|
||||
/// Transaction information
|
||||
///
|
||||
/// Retrieve complete transaction data by transaction ID (txid). Returns inputs, outputs, fee, size, and confirmation status.
|
||||
|
||||
@@ -417,7 +417,11 @@ impl Query {
|
||||
first_seen: None,
|
||||
};
|
||||
|
||||
blocks.push(BlockInfoV1 { info, extras });
|
||||
blocks.push(BlockInfoV1 {
|
||||
info,
|
||||
stale: false,
|
||||
extras,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(blocks)
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::{collections::BTreeMap, sync::LazyLock};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_traversable::TreeNode;
|
||||
use brk_types::{
|
||||
Date, DetailedSeriesCount, Epoch, Etag, Format, Halving, Height, Index, IndexInfo, LegacyValue,
|
||||
Limit, Output, OutputLegacy, PaginatedSeries, Pagination, PaginationIndex, RangeIndex,
|
||||
RangeMap, SearchQuery, SeriesData, SeriesInfo, SeriesName, SeriesOutput, SeriesOutputLegacy,
|
||||
SeriesSelection, Timestamp, Version,
|
||||
BlockHashPrefix, Date, DetailedSeriesCount, Epoch, Etag, Format, Halving, Height, Index,
|
||||
IndexInfo, LegacyValue, Limit, Output, OutputLegacy, PaginatedSeries, Pagination,
|
||||
PaginationIndex, RangeIndex, RangeMap, SearchQuery, SeriesData, SeriesInfo, SeriesName,
|
||||
SeriesOutput, SeriesOutputLegacy, SeriesSelection, Timestamp, Version,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use vecdb::{AnyExportableVec, ReadableVec};
|
||||
@@ -204,7 +204,7 @@ impl Query {
|
||||
total,
|
||||
start,
|
||||
end,
|
||||
height: *self.height(),
|
||||
hash_prefix: self.tip_hash_prefix(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -458,12 +458,12 @@ pub struct ResolvedQuery {
|
||||
pub total: usize,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub height: u32,
|
||||
pub hash_prefix: BlockHashPrefix,
|
||||
}
|
||||
|
||||
impl ResolvedQuery {
|
||||
pub fn etag(&self) -> Etag {
|
||||
Etag::from_series(self.version, self.total, self.start, self.end, self.height)
|
||||
Etag::from_series(self.version, self.total, self.end, self.hash_prefix)
|
||||
}
|
||||
|
||||
pub fn format(&self) -> Format {
|
||||
|
||||
@@ -4,7 +4,7 @@ use brk_types::{
|
||||
BlockHash, Height, MerkleProof, Timestamp, Transaction, TxInIndex, TxIndex, TxOutIndex,
|
||||
TxOutspend, TxStatus, Txid, TxidPrefix, Vin, Vout,
|
||||
};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -23,6 +23,19 @@ impl Query {
|
||||
.ok_or(Error::UnknownTxid)
|
||||
}
|
||||
|
||||
pub fn txid_by_index(&self, index: TxIndex) -> Result<Txid> {
|
||||
let len = self.indexer().vecs.transactions.txid.len();
|
||||
if index.to_usize() >= len {
|
||||
return Err(Error::OutOfRange("Transaction index out of range".into()));
|
||||
}
|
||||
self.indexer()
|
||||
.vecs
|
||||
.transactions
|
||||
.txid
|
||||
.collect_one(index)
|
||||
.ok_or_else(|| Error::OutOfRange("Transaction index out of range".into()))
|
||||
}
|
||||
|
||||
/// Resolve a txid to (TxIndex, Height).
|
||||
pub fn resolve_tx(&self, txid: &Txid) -> Result<(TxIndex, Height)> {
|
||||
let tx_index = self.resolve_tx_index(txid)?;
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
AppState, CacheStrategy,
|
||||
cache::CacheParams,
|
||||
extended::{ResponseExtended, TransformResponseExtended},
|
||||
params::{TxidParam, TxidVout, TxidsParam},
|
||||
params::{TxIndexParam, TxidParam, TxidVout, TxidsParam},
|
||||
};
|
||||
|
||||
pub trait TxRoutes {
|
||||
@@ -23,6 +23,24 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
fn add_tx_routes(self) -> Self {
|
||||
self
|
||||
.api_route(
|
||||
"/api/tx-index/{index}",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, Path(param): Path<TxIndexParam>, State(state): State<AppState>| {
|
||||
state.cached_text(&headers, CacheStrategy::Immutable(Version::ONE), &uri, move |q| q.txid_by_index(param.index).map(|t| t.to_string())).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_tx_by_index")
|
||||
.transactions_tag()
|
||||
.summary("Txid by index")
|
||||
.description("Retrieve the transaction ID (txid) at a given global transaction index. Returns the txid as plain text.")
|
||||
.text_response()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/cpfp/{txid}",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, Path(param): Path<TxidParam>, State(state): State<AppState>| {
|
||||
|
||||
@@ -197,7 +197,6 @@ impl Server {
|
||||
let router = router
|
||||
.with_state(state)
|
||||
.merge(website_router)
|
||||
.layer(compression_layer)
|
||||
.layer(response_time_layer)
|
||||
.layer(trace_layer)
|
||||
.layer(CatchPanicLayer::custom(|panic: Box<dyn Any + Send>| {
|
||||
@@ -213,6 +212,7 @@ impl Server {
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.layer(json_error_layer)
|
||||
.layer(compression_layer)
|
||||
.layer(CorsLayer::permissive())
|
||||
.layer(axum::middleware::from_fn(
|
||||
async |request: Request<Body>, next: Next| -> Response<Body> {
|
||||
|
||||
@@ -11,6 +11,7 @@ mod pool_slug_param;
|
||||
mod series_param;
|
||||
mod time_period_param;
|
||||
mod timestamp_param;
|
||||
mod tx_index_param;
|
||||
mod txid_param;
|
||||
mod txid_vout;
|
||||
mod txids_param;
|
||||
@@ -29,6 +30,7 @@ pub use pool_slug_param::*;
|
||||
pub use series_param::*;
|
||||
pub use time_period_param::*;
|
||||
pub use timestamp_param::*;
|
||||
pub use tx_index_param::*;
|
||||
pub use txid_param::*;
|
||||
pub use txid_vout::*;
|
||||
pub use txids_param::*;
|
||||
|
||||
10
crates/brk_server/src/params/tx_index_param.rs
Normal file
10
crates/brk_server/src/params/tx_index_param.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use brk_types::TxIndex;
|
||||
|
||||
/// Transaction index path parameter
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct TxIndexParam {
|
||||
pub index: TxIndex,
|
||||
}
|
||||
@@ -72,7 +72,7 @@ impl AddrValidation {
|
||||
let output_type = OutputType::from(&script);
|
||||
let script_hex = script.as_bytes().to_lower_hex_string();
|
||||
|
||||
let is_script = matches!(output_type, OutputType::P2SH);
|
||||
let is_script = matches!(output_type, OutputType::P2SH | OutputType::P2TR);
|
||||
let is_witness = matches!(
|
||||
output_type,
|
||||
OutputType::P2WPKH | OutputType::P2WSH | OutputType::P2TR | OutputType::P2A
|
||||
|
||||
@@ -10,6 +10,10 @@ pub struct BlockInfoV1 {
|
||||
#[serde(flatten)]
|
||||
pub info: BlockInfo,
|
||||
|
||||
/// Whether this block has been replaced by a longer chain
|
||||
#[serde(default)]
|
||||
pub stale: bool,
|
||||
|
||||
/// Extended block data
|
||||
pub extras: BlockExtras,
|
||||
}
|
||||
|
||||
@@ -52,4 +52,5 @@ impl DataRange {
|
||||
pub fn limit(&self) -> Option<Limit> {
|
||||
self.limit
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::fmt;
|
||||
|
||||
use super::{BlockHashPrefix, Version};
|
||||
|
||||
/// HTTP ETag value.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Etag(String);
|
||||
@@ -40,27 +42,20 @@ impl From<&str> for Etag {
|
||||
}
|
||||
|
||||
impl Etag {
|
||||
/// Create ETag from series data response info.
|
||||
///
|
||||
/// Format varies based on whether the slice touches the end:
|
||||
/// - Slice ends before total: `{version:x}-{start}-{end}` (len irrelevant, data won't change if series grows)
|
||||
/// - Slice reaches the end: `{version:x}-{start}-{total}-{height}` (includes height since last value may be recomputed each block)
|
||||
///
|
||||
/// `version` is the series version for single queries, or the sum of versions for bulk queries.
|
||||
/// Tail uses hash prefix (changes per-block and on reorgs),
|
||||
/// non-tail uses total (changes per-block).
|
||||
pub fn from_series(
|
||||
version: super::Version,
|
||||
version: Version,
|
||||
total: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
height: u32,
|
||||
hash_prefix: BlockHashPrefix,
|
||||
) -> Self {
|
||||
let v = u32::from(version);
|
||||
if end < total {
|
||||
// Fixed window not at the end - len doesn't matter
|
||||
Self(format!("{v:x}-{start}-{end}"))
|
||||
if end >= total {
|
||||
let h = *hash_prefix;
|
||||
Self(format!("v{v}-{h:x}"))
|
||||
} else {
|
||||
// Fetching up to current end - include height since last value may change each block
|
||||
Self(format!("{v:x}-{start}-{total}-{height}"))
|
||||
Self(format!("v{v}-{total}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ Matches mempool.space/bitcoin-cli behavior.
|
||||
* @property {Weight} weight - Block weight in weight units
|
||||
* @property {BlockHash} previousblockhash - Previous block hash
|
||||
* @property {Timestamp} mediantime - Median time of the last 11 blocks
|
||||
* @property {boolean=} stale - Whether this block has been replaced by a longer chain
|
||||
* @property {BlockExtras} extras - Extended block data
|
||||
*/
|
||||
/**
|
||||
@@ -1085,6 +1086,12 @@ Matches mempool.space/bitcoin-cli behavior.
|
||||
*
|
||||
* @typedef {number} TxIndex
|
||||
*/
|
||||
/**
|
||||
* Transaction index path parameter
|
||||
*
|
||||
* @typedef {Object} TxIndexParam
|
||||
* @property {TxIndex} index
|
||||
*/
|
||||
/**
|
||||
* Transaction output
|
||||
*
|
||||
@@ -5104,7 +5111,6 @@ function createTransferPattern(client, acc) {
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Height
|
||||
* @property {SeriesPattern18<Height>} identity
|
||||
* @property {SeriesPattern18<Minute10>} minute10
|
||||
* @property {SeriesPattern18<Minute30>} minute30
|
||||
* @property {SeriesPattern18<Hour1>} hour1
|
||||
@@ -5125,99 +5131,83 @@ function createTransferPattern(client, acc) {
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Epoch
|
||||
* @property {SeriesPattern17<Epoch>} identity
|
||||
* @property {SeriesPattern17<Height>} firstHeight
|
||||
* @property {SeriesPattern17<StoredU64>} heightCount
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Halving
|
||||
* @property {SeriesPattern16<Halving>} identity
|
||||
* @property {SeriesPattern16<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Minute10
|
||||
* @property {SeriesPattern3<Minute10>} identity
|
||||
* @property {SeriesPattern3<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Minute30
|
||||
* @property {SeriesPattern4<Minute30>} identity
|
||||
* @property {SeriesPattern4<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Hour1
|
||||
* @property {SeriesPattern5<Hour1>} identity
|
||||
* @property {SeriesPattern5<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Hour4
|
||||
* @property {SeriesPattern6<Hour4>} identity
|
||||
* @property {SeriesPattern6<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Hour12
|
||||
* @property {SeriesPattern7<Hour12>} identity
|
||||
* @property {SeriesPattern7<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Day1
|
||||
* @property {SeriesPattern8<Day1>} identity
|
||||
* @property {SeriesPattern8<Date>} date
|
||||
* @property {SeriesPattern8<Height>} firstHeight
|
||||
* @property {SeriesPattern8<StoredU64>} heightCount
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Day3
|
||||
* @property {SeriesPattern9<Day3>} identity
|
||||
* @property {SeriesPattern9<Date>} date
|
||||
* @property {SeriesPattern9<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Week1
|
||||
* @property {SeriesPattern10<Week1>} identity
|
||||
* @property {SeriesPattern10<Date>} date
|
||||
* @property {SeriesPattern10<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Month1
|
||||
* @property {SeriesPattern11<Month1>} identity
|
||||
* @property {SeriesPattern11<Date>} date
|
||||
* @property {SeriesPattern11<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Month3
|
||||
* @property {SeriesPattern12<Month3>} identity
|
||||
* @property {SeriesPattern12<Date>} date
|
||||
* @property {SeriesPattern12<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Month6
|
||||
* @property {SeriesPattern13<Month6>} identity
|
||||
* @property {SeriesPattern13<Date>} date
|
||||
* @property {SeriesPattern13<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Year1
|
||||
* @property {SeriesPattern14<Year1>} identity
|
||||
* @property {SeriesPattern14<Date>} date
|
||||
* @property {SeriesPattern14<Height>} firstHeight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SeriesTree_Indexes_Year10
|
||||
* @property {SeriesPattern15<Year10>} identity
|
||||
* @property {SeriesPattern15<Date>} date
|
||||
* @property {SeriesPattern15<Height>} firstHeight
|
||||
*/
|
||||
@@ -8274,7 +8264,6 @@ class BrkClient extends BrkClientBase {
|
||||
},
|
||||
},
|
||||
height: {
|
||||
identity: createSeriesPattern18(this, 'height'),
|
||||
minute10: createSeriesPattern18(this, 'minute10'),
|
||||
minute30: createSeriesPattern18(this, 'minute30'),
|
||||
hour1: createSeriesPattern18(this, 'hour1'),
|
||||
@@ -8293,71 +8282,55 @@ class BrkClient extends BrkClientBase {
|
||||
txIndexCount: createSeriesPattern18(this, 'tx_index_count'),
|
||||
},
|
||||
epoch: {
|
||||
identity: createSeriesPattern17(this, 'epoch'),
|
||||
firstHeight: createSeriesPattern17(this, 'first_height'),
|
||||
heightCount: createSeriesPattern17(this, 'height_count'),
|
||||
},
|
||||
halving: {
|
||||
identity: createSeriesPattern16(this, 'halving'),
|
||||
firstHeight: createSeriesPattern16(this, 'first_height'),
|
||||
},
|
||||
minute10: {
|
||||
identity: createSeriesPattern3(this, 'minute10_index'),
|
||||
firstHeight: createSeriesPattern3(this, 'first_height'),
|
||||
},
|
||||
minute30: {
|
||||
identity: createSeriesPattern4(this, 'minute30_index'),
|
||||
firstHeight: createSeriesPattern4(this, 'first_height'),
|
||||
},
|
||||
hour1: {
|
||||
identity: createSeriesPattern5(this, 'hour1_index'),
|
||||
firstHeight: createSeriesPattern5(this, 'first_height'),
|
||||
},
|
||||
hour4: {
|
||||
identity: createSeriesPattern6(this, 'hour4_index'),
|
||||
firstHeight: createSeriesPattern6(this, 'first_height'),
|
||||
},
|
||||
hour12: {
|
||||
identity: createSeriesPattern7(this, 'hour12_index'),
|
||||
firstHeight: createSeriesPattern7(this, 'first_height'),
|
||||
},
|
||||
day1: {
|
||||
identity: createSeriesPattern8(this, 'day1_index'),
|
||||
date: createSeriesPattern8(this, 'date'),
|
||||
firstHeight: createSeriesPattern8(this, 'first_height'),
|
||||
heightCount: createSeriesPattern8(this, 'height_count'),
|
||||
},
|
||||
day3: {
|
||||
identity: createSeriesPattern9(this, 'day3_index'),
|
||||
date: createSeriesPattern9(this, 'date'),
|
||||
firstHeight: createSeriesPattern9(this, 'first_height'),
|
||||
},
|
||||
week1: {
|
||||
identity: createSeriesPattern10(this, 'week1_index'),
|
||||
date: createSeriesPattern10(this, 'date'),
|
||||
firstHeight: createSeriesPattern10(this, 'first_height'),
|
||||
},
|
||||
month1: {
|
||||
identity: createSeriesPattern11(this, 'month1_index'),
|
||||
date: createSeriesPattern11(this, 'date'),
|
||||
firstHeight: createSeriesPattern11(this, 'first_height'),
|
||||
},
|
||||
month3: {
|
||||
identity: createSeriesPattern12(this, 'month3_index'),
|
||||
date: createSeriesPattern12(this, 'date'),
|
||||
firstHeight: createSeriesPattern12(this, 'first_height'),
|
||||
},
|
||||
month6: {
|
||||
identity: createSeriesPattern13(this, 'month6_index'),
|
||||
date: createSeriesPattern13(this, 'date'),
|
||||
firstHeight: createSeriesPattern13(this, 'first_height'),
|
||||
},
|
||||
year1: {
|
||||
identity: createSeriesPattern14(this, 'year1_index'),
|
||||
date: createSeriesPattern14(this, 'date'),
|
||||
firstHeight: createSeriesPattern14(this, 'first_height'),
|
||||
},
|
||||
year10: {
|
||||
identity: createSeriesPattern15(this, 'year10_index'),
|
||||
date: createSeriesPattern15(this, 'date'),
|
||||
firstHeight: createSeriesPattern15(this, 'first_height'),
|
||||
},
|
||||
@@ -9496,7 +9469,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getApi({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api.json`, { signal, onUpdate });
|
||||
const path = `/api.json`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9513,7 +9487,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<AddrStats>}
|
||||
*/
|
||||
async getAddress(address, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/address/${address}`, { signal, onUpdate });
|
||||
const path = `/api/address/${address}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9574,7 +9549,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Txid[]>}
|
||||
*/
|
||||
async getAddressMempoolTxs(address, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/address/${address}/txs/mempool`, { signal, onUpdate });
|
||||
const path = `/api/address/${address}/txs/mempool`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9591,7 +9567,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Utxo[]>}
|
||||
*/
|
||||
async getAddressUtxos(address, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/address/${address}/utxo`, { signal, onUpdate });
|
||||
const path = `/api/address/${address}/utxo`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9608,7 +9585,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getBlockByHeight(height, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block-height/${height}`, { signal, onUpdate });
|
||||
const path = `/api/block-height/${height}`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9625,7 +9603,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfo>}
|
||||
*/
|
||||
async getBlock(hash, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9642,7 +9621,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getBlockHeader(hash, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}/header`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}/header`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9659,7 +9639,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getBlockRaw(hash, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}/raw`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}/raw`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9676,7 +9657,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockStatus>}
|
||||
*/
|
||||
async getBlockStatus(hash, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}/status`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}/status`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9694,7 +9676,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getBlockTxid(hash, index, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}/txid/${index}`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}/txid/${index}`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9711,7 +9694,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Txid[]>}
|
||||
*/
|
||||
async getBlockTxids(hash, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}/txids`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}/txids`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9728,7 +9712,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Transaction[]>}
|
||||
*/
|
||||
async getBlockTxs(hash, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}/txs`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}/txs`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9746,7 +9731,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Transaction[]>}
|
||||
*/
|
||||
async getBlockTxsFromIndex(hash, start_index, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/block/${hash}/txs/${start_index}`, { signal, onUpdate });
|
||||
const path = `/api/block/${hash}/txs/${start_index}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9761,7 +9747,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfo[]>}
|
||||
*/
|
||||
async getBlocks({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/blocks`, { signal, onUpdate });
|
||||
const path = `/api/blocks`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9776,7 +9763,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getBlockTipHash({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/blocks/tip/hash`, { signal, onUpdate });
|
||||
const path = `/api/blocks/tip/hash`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9791,7 +9779,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getBlockTipHeight({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/blocks/tip/height`, { signal, onUpdate });
|
||||
const path = `/api/blocks/tip/height`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9808,7 +9797,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfo[]>}
|
||||
*/
|
||||
async getBlocksFromHeight(height, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/blocks/${height}`, { signal, onUpdate });
|
||||
const path = `/api/blocks/${height}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9823,7 +9813,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<MempoolInfo>}
|
||||
*/
|
||||
async getMempool({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/mempool`, { signal, onUpdate });
|
||||
const path = `/api/mempool`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9836,7 +9827,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Dollars>}
|
||||
*/
|
||||
async getLivePrice({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/mempool/price`, { signal, onUpdate });
|
||||
const path = `/api/mempool/price`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9851,7 +9843,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<MempoolRecentTx[]>}
|
||||
*/
|
||||
async getMempoolRecent({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/mempool/recent`, { signal, onUpdate });
|
||||
const path = `/api/mempool/recent`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9866,7 +9859,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Txid[]>}
|
||||
*/
|
||||
async getMempoolTxids({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/mempool/txids`, { signal, onUpdate });
|
||||
const path = `/api/mempool/txids`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9879,7 +9873,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<TreeNode>}
|
||||
*/
|
||||
async getSeriesTree({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series`, { signal, onUpdate });
|
||||
const path = `/api/series`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9908,9 +9903,7 @@ class BrkClient extends BrkClientBase {
|
||||
if (format !== undefined) params.set('format', String(format));
|
||||
const query = params.toString();
|
||||
const path = `/api/series/bulk${query ? '?' + query : ''}`;
|
||||
if (format === 'csv') {
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
if (format === 'csv') return this.getText(path, { signal });
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
@@ -9924,7 +9917,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async getCostBasisCohorts({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/cost-basis`, { signal, onUpdate });
|
||||
const path = `/api/series/cost-basis`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9939,7 +9933,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Date[]>}
|
||||
*/
|
||||
async getCostBasisDates(cohort, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/cost-basis/${cohort}/dates`, { signal, onUpdate });
|
||||
const path = `/api/series/cost-basis/${cohort}/dates`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9979,7 +9974,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<SeriesCount[]>}
|
||||
*/
|
||||
async getSeriesCount({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/count`, { signal, onUpdate });
|
||||
const path = `/api/series/count`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9992,7 +9988,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<IndexInfo[]>}
|
||||
*/
|
||||
async getIndexes({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/indexes`, { signal, onUpdate });
|
||||
const path = `/api/series/indexes`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10049,7 +10046,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<SeriesInfo>}
|
||||
*/
|
||||
async getSeriesInfo(series, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/${series}`, { signal, onUpdate });
|
||||
const path = `/api/series/${series}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10076,9 +10074,7 @@ class BrkClient extends BrkClientBase {
|
||||
if (format !== undefined) params.set('format', String(format));
|
||||
const query = params.toString();
|
||||
const path = `/api/series/${series}/${index}${query ? '?' + query : ''}`;
|
||||
if (format === 'csv') {
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
if (format === 'csv') return this.getText(path, { signal });
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
@@ -10106,9 +10102,7 @@ class BrkClient extends BrkClientBase {
|
||||
if (format !== undefined) params.set('format', String(format));
|
||||
const query = params.toString();
|
||||
const path = `/api/series/${series}/${index}/data${query ? '?' + query : ''}`;
|
||||
if (format === 'csv') {
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
if (format === 'csv') return this.getText(path, { signal });
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
@@ -10125,7 +10119,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getSeriesLatest(series, index, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/${series}/${index}/latest`, { signal, onUpdate });
|
||||
const path = `/api/series/${series}/${index}/latest`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10141,7 +10136,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async getSeriesLen(series, index, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/${series}/${index}/len`, { signal, onUpdate });
|
||||
const path = `/api/series/${series}/${index}/len`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10157,7 +10153,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Version>}
|
||||
*/
|
||||
async getSeriesVersion(series, index, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/series/${series}/${index}/version`, { signal, onUpdate });
|
||||
const path = `/api/series/${series}/${index}/version`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10170,7 +10167,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<DiskUsage>}
|
||||
*/
|
||||
async getDiskUsage({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/server/disk`, { signal, onUpdate });
|
||||
const path = `/api/server/disk`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10183,7 +10181,24 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<SyncStatus>}
|
||||
*/
|
||||
async getSyncStatus({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/server/sync`, { signal, onUpdate });
|
||||
const path = `/api/server/sync`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
* Txid by index
|
||||
*
|
||||
* Retrieve the transaction ID (txid) at a given global transaction index. Returns the txid as plain text.
|
||||
*
|
||||
* Endpoint: `GET /api/tx-index/{index}`
|
||||
*
|
||||
* @param {TxIndex} index
|
||||
* @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options]
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getTxByIndex(index, { signal, onUpdate } = {}) {
|
||||
const path = `/api/tx-index/${index}`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10200,7 +10215,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Transaction>}
|
||||
*/
|
||||
async getTx(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10217,7 +10233,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getTxHex(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}/hex`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}/hex`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10234,7 +10251,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<MerkleProof>}
|
||||
*/
|
||||
async getTxMerkleProof(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}/merkle-proof`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}/merkle-proof`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10251,7 +10269,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getTxMerkleblockProof(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}/merkleblock-proof`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}/merkleblock-proof`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10269,7 +10288,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<TxOutspend>}
|
||||
*/
|
||||
async getTxOutspend(txid, vout, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}/outspend/${vout}`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}/outspend/${vout}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10286,7 +10306,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<TxOutspend[]>}
|
||||
*/
|
||||
async getTxOutspends(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}/outspends`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}/outspends`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10303,7 +10324,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getTxRaw(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}/raw`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}/raw`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10320,7 +10342,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<TxStatus>}
|
||||
*/
|
||||
async getTxStatus(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/tx/${txid}/status`, { signal, onUpdate });
|
||||
const path = `/api/tx/${txid}/status`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10337,7 +10360,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfoV1>}
|
||||
*/
|
||||
async getBlockV1(hash, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/block/${hash}`, { signal, onUpdate });
|
||||
const path = `/api/v1/block/${hash}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10352,7 +10376,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfoV1[]>}
|
||||
*/
|
||||
async getBlocksV1({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/blocks`, { signal, onUpdate });
|
||||
const path = `/api/v1/blocks`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10369,7 +10394,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfoV1[]>}
|
||||
*/
|
||||
async getBlocksV1FromHeight(height, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/blocks/${height}`, { signal, onUpdate });
|
||||
const path = `/api/v1/blocks/${height}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10386,7 +10412,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<CpfpInfo>}
|
||||
*/
|
||||
async getCpfp(txid, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/cpfp/${txid}`, { signal, onUpdate });
|
||||
const path = `/api/v1/cpfp/${txid}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10401,7 +10428,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<DifficultyAdjustment>}
|
||||
*/
|
||||
async getDifficultyAdjustment({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/difficulty-adjustment`, { signal, onUpdate });
|
||||
const path = `/api/v1/difficulty-adjustment`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10416,7 +10444,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<MempoolBlock[]>}
|
||||
*/
|
||||
async getMempoolBlocks({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/fees/mempool-blocks`, { signal, onUpdate });
|
||||
const path = `/api/v1/fees/mempool-blocks`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10431,7 +10460,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<RecommendedFees>}
|
||||
*/
|
||||
async getPreciseFees({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/fees/precise`, { signal, onUpdate });
|
||||
const path = `/api/v1/fees/precise`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10446,7 +10476,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<RecommendedFees>}
|
||||
*/
|
||||
async getRecommendedFees({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/fees/recommended`, { signal, onUpdate });
|
||||
const path = `/api/v1/fees/recommended`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10484,7 +10515,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockFeeRatesEntry[]>}
|
||||
*/
|
||||
async getBlockFeeRates(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/blocks/fee-rates/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/blocks/fee-rates/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10501,7 +10533,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockFeesEntry[]>}
|
||||
*/
|
||||
async getBlockFees(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/blocks/fees/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/blocks/fees/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10518,7 +10551,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockRewardsEntry[]>}
|
||||
*/
|
||||
async getBlockRewards(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/blocks/rewards/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/blocks/rewards/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10535,7 +10569,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockSizesWeights>}
|
||||
*/
|
||||
async getBlockSizesWeights(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/blocks/sizes-weights/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/blocks/sizes-weights/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10552,7 +10587,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockTimestamp>}
|
||||
*/
|
||||
async getBlockByTimestamp(timestamp, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/blocks/timestamp/${timestamp}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/blocks/timestamp/${timestamp}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10567,7 +10603,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<DifficultyAdjustmentEntry[]>}
|
||||
*/
|
||||
async getDifficultyAdjustments({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/difficulty-adjustments`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/difficulty-adjustments`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10584,7 +10621,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<DifficultyAdjustmentEntry[]>}
|
||||
*/
|
||||
async getDifficultyAdjustmentsByPeriod(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/difficulty-adjustments/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/difficulty-adjustments/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10599,7 +10637,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<HashrateSummary>}
|
||||
*/
|
||||
async getHashrate({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/hashrate`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/hashrate`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10614,7 +10653,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<PoolHashrateEntry[]>}
|
||||
*/
|
||||
async getPoolsHashrate({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/hashrate/pools`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/hashrate/pools`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10631,7 +10671,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<PoolHashrateEntry[]>}
|
||||
*/
|
||||
async getPoolsHashrateByPeriod(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/hashrate/pools/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/hashrate/pools/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10648,7 +10689,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<HashrateSummary>}
|
||||
*/
|
||||
async getHashrateByPeriod(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/hashrate/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/hashrate/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10665,7 +10707,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<PoolDetail>}
|
||||
*/
|
||||
async getPool(slug, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/pool/${slug}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/pool/${slug}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10682,7 +10725,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfoV1[]>}
|
||||
*/
|
||||
async getPoolBlocks(slug, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/pool/${slug}/blocks`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/pool/${slug}/blocks`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10700,7 +10744,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<BlockInfoV1[]>}
|
||||
*/
|
||||
async getPoolBlocksFrom(slug, height, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/pool/${slug}/blocks/${height}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/pool/${slug}/blocks/${height}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10717,7 +10762,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<PoolHashrateEntry[]>}
|
||||
*/
|
||||
async getPoolHashrate(slug, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/pool/${slug}/hashrate`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/pool/${slug}/hashrate`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10732,7 +10778,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<PoolInfo[]>}
|
||||
*/
|
||||
async getPools({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/pools`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/pools`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10749,7 +10796,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<PoolsSummary>}
|
||||
*/
|
||||
async getPoolStats(time_period, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/pools/${time_period}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/pools/${time_period}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10766,7 +10814,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<RewardStats>}
|
||||
*/
|
||||
async getRewardStats(block_count, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/mining/reward-stats/${block_count}`, { signal, onUpdate });
|
||||
const path = `/api/v1/mining/reward-stats/${block_count}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10781,7 +10830,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Prices>}
|
||||
*/
|
||||
async getPrices({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/prices`, { signal, onUpdate });
|
||||
const path = `/api/v1/prices`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10796,7 +10846,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async getTransactionTimes({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/transaction-times`, { signal, onUpdate });
|
||||
const path = `/api/v1/transaction-times`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10813,7 +10864,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<AddrValidation>}
|
||||
*/
|
||||
async validateAddress(address, { signal, onUpdate } = {}) {
|
||||
return this.getJson(`/api/v1/validate-address/${address}`, { signal, onUpdate });
|
||||
const path = `/api/v1/validate-address/${address}`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10826,7 +10878,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<Health>}
|
||||
*/
|
||||
async getHealth({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/health`, { signal, onUpdate });
|
||||
const path = `/health`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10839,7 +10892,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getOpenapi({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/openapi.json`, { signal, onUpdate });
|
||||
const path = `/openapi.json`;
|
||||
return this.getText(path, { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10852,7 +10906,8 @@ class BrkClient extends BrkClientBase {
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async getVersion({ signal, onUpdate } = {}) {
|
||||
return this.getJson(`/version`, { signal, onUpdate });
|
||||
const path = `/version`;
|
||||
return this.getJson(path, { signal, onUpdate });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -524,6 +524,7 @@ class BlockInfoV1(TypedDict):
|
||||
weight: Block weight in weight units
|
||||
previousblockhash: Previous block hash
|
||||
mediantime: Median time of the last 11 blocks
|
||||
stale: Whether this block has been replaced by a longer chain
|
||||
extras: Extended block data
|
||||
"""
|
||||
id: BlockHash
|
||||
@@ -539,6 +540,7 @@ class BlockInfoV1(TypedDict):
|
||||
weight: Weight
|
||||
previousblockhash: BlockHash
|
||||
mediantime: Timestamp
|
||||
stale: bool
|
||||
extras: BlockExtras
|
||||
|
||||
class BlockRewardsEntry(TypedDict):
|
||||
@@ -1471,6 +1473,12 @@ class Transaction(TypedDict):
|
||||
fee: Sats
|
||||
status: TxStatus
|
||||
|
||||
class TxIndexParam(TypedDict):
|
||||
"""
|
||||
Transaction index path parameter
|
||||
"""
|
||||
index: TxIndex
|
||||
|
||||
class TxOutspend(TypedDict):
|
||||
"""
|
||||
Status of an output indicating whether it has been spent
|
||||
@@ -4394,7 +4402,6 @@ class SeriesTree_Indexes_Height:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern18[Height] = SeriesPattern18(client, 'height')
|
||||
self.minute10: SeriesPattern18[Minute10] = SeriesPattern18(client, 'minute10')
|
||||
self.minute30: SeriesPattern18[Minute30] = SeriesPattern18(client, 'minute30')
|
||||
self.hour1: SeriesPattern18[Hour1] = SeriesPattern18(client, 'hour1')
|
||||
@@ -4416,73 +4423,62 @@ class SeriesTree_Indexes_Epoch:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern17[Epoch] = SeriesPattern17(client, 'epoch')
|
||||
self.first_height: SeriesPattern17[Height] = SeriesPattern17(client, 'first_height')
|
||||
self.height_count: SeriesPattern17[StoredU64] = SeriesPattern17(client, 'height_count')
|
||||
|
||||
class SeriesTree_Indexes_Halving:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern16[Halving] = SeriesPattern16(client, 'halving')
|
||||
self.first_height: SeriesPattern16[Height] = SeriesPattern16(client, 'first_height')
|
||||
|
||||
class SeriesTree_Indexes_Minute10:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern3[Minute10] = SeriesPattern3(client, 'minute10_index')
|
||||
self.first_height: SeriesPattern3[Height] = SeriesPattern3(client, 'first_height')
|
||||
|
||||
class SeriesTree_Indexes_Minute30:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern4[Minute30] = SeriesPattern4(client, 'minute30_index')
|
||||
self.first_height: SeriesPattern4[Height] = SeriesPattern4(client, 'first_height')
|
||||
|
||||
class SeriesTree_Indexes_Hour1:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern5[Hour1] = SeriesPattern5(client, 'hour1_index')
|
||||
self.first_height: SeriesPattern5[Height] = SeriesPattern5(client, 'first_height')
|
||||
|
||||
class SeriesTree_Indexes_Hour4:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern6[Hour4] = SeriesPattern6(client, 'hour4_index')
|
||||
self.first_height: SeriesPattern6[Height] = SeriesPattern6(client, 'first_height')
|
||||
|
||||
class SeriesTree_Indexes_Hour12:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern7[Hour12] = SeriesPattern7(client, 'hour12_index')
|
||||
self.first_height: SeriesPattern7[Height] = SeriesPattern7(client, 'first_height')
|
||||
|
||||
class SeriesTree_Indexes_Day1:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern8[Day1] = SeriesPattern8(client, 'day1_index')
|
||||
self.date: SeriesPattern8[Date] = SeriesPattern8(client, 'date')
|
||||
self.first_height: SeriesPattern8[Height] = SeriesPattern8(client, 'first_height')
|
||||
self.height_count: SeriesPattern8[StoredU64] = SeriesPattern8(client, 'height_count')
|
||||
|
||||
class SeriesTree_Indexes_Day3:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern9[Day3] = SeriesPattern9(client, 'day3_index')
|
||||
self.date: SeriesPattern9[Date] = SeriesPattern9(client, 'date')
|
||||
self.first_height: SeriesPattern9[Height] = SeriesPattern9(client, 'first_height')
|
||||
|
||||
class SeriesTree_Indexes_Week1:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern10[Week1] = SeriesPattern10(client, 'week1_index')
|
||||
self.date: SeriesPattern10[Date] = SeriesPattern10(client, 'date')
|
||||
self.first_height: SeriesPattern10[Height] = SeriesPattern10(client, 'first_height')
|
||||
|
||||
@@ -4490,7 +4486,6 @@ class SeriesTree_Indexes_Month1:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern11[Month1] = SeriesPattern11(client, 'month1_index')
|
||||
self.date: SeriesPattern11[Date] = SeriesPattern11(client, 'date')
|
||||
self.first_height: SeriesPattern11[Height] = SeriesPattern11(client, 'first_height')
|
||||
|
||||
@@ -4498,7 +4493,6 @@ class SeriesTree_Indexes_Month3:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern12[Month3] = SeriesPattern12(client, 'month3_index')
|
||||
self.date: SeriesPattern12[Date] = SeriesPattern12(client, 'date')
|
||||
self.first_height: SeriesPattern12[Height] = SeriesPattern12(client, 'first_height')
|
||||
|
||||
@@ -4506,7 +4500,6 @@ class SeriesTree_Indexes_Month6:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern13[Month6] = SeriesPattern13(client, 'month6_index')
|
||||
self.date: SeriesPattern13[Date] = SeriesPattern13(client, 'date')
|
||||
self.first_height: SeriesPattern13[Height] = SeriesPattern13(client, 'first_height')
|
||||
|
||||
@@ -4514,7 +4507,6 @@ class SeriesTree_Indexes_Year1:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern14[Year1] = SeriesPattern14(client, 'year1_index')
|
||||
self.date: SeriesPattern14[Date] = SeriesPattern14(client, 'date')
|
||||
self.first_height: SeriesPattern14[Height] = SeriesPattern14(client, 'first_height')
|
||||
|
||||
@@ -4522,7 +4514,6 @@ class SeriesTree_Indexes_Year10:
|
||||
"""Series tree node."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.identity: SeriesPattern15[Year10] = SeriesPattern15(client, 'year10_index')
|
||||
self.date: SeriesPattern15[Date] = SeriesPattern15(client, 'date')
|
||||
self.first_height: SeriesPattern15[Height] = SeriesPattern15(client, 'first_height')
|
||||
|
||||
@@ -7669,6 +7660,14 @@ class BrkClient(BrkClientBase):
|
||||
Endpoint: `GET /api/server/sync`"""
|
||||
return self.get_json('/api/server/sync')
|
||||
|
||||
def get_tx_by_index(self, index: TxIndex) -> str:
|
||||
"""Txid by index.
|
||||
|
||||
Retrieve the transaction ID (txid) at a given global transaction index. Returns the txid as plain text.
|
||||
|
||||
Endpoint: `GET /api/tx-index/{index}`"""
|
||||
return self.get_text(f'/api/tx-index/{index}')
|
||||
|
||||
def get_tx(self, txid: Txid) -> Transaction:
|
||||
"""Transaction information.
|
||||
|
||||
|
||||
338
scripts/mempool_compat/conftest.py
Normal file
338
scripts/mempool_compat/conftest.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
Mempool.space API compatibility tests.
|
||||
|
||||
Compares every brk mempool_space endpoint against the real mempool.space API
|
||||
using live blockchain data — nothing is hardcoded or deterministic.
|
||||
|
||||
Usage:
|
||||
cd scripts/mempool_compat
|
||||
uv run pytest -sv # all tests, verbose
|
||||
uv run pytest -sv test_blocks.py # one category
|
||||
uv run pytest -sv -k "test_block_header" # one test
|
||||
BRK_URL=http://host:port uv run pytest -sv # custom brk server
|
||||
|
||||
Environment variables:
|
||||
BRK_URL brk server base URL (default: http://localhost:3000)
|
||||
MEMPOOL_URL mempool.space base URL (default: https://mempool.space)
|
||||
RATE_LIMIT seconds between mempool.space requests (default: 0.5)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Set
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
BRK_BASE = os.environ.get("BRK_URL", "http://localhost:3000")
|
||||
MEMPOOL_BASE = os.environ.get("MEMPOOL_URL", "https://mempool.space")
|
||||
RATE_LIMIT = float(os.environ.get("RATE_LIMIT", "0.5"))
|
||||
|
||||
|
||||
# ── API client ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ApiClient:
|
||||
"""HTTP client for a single API server with optional rate limiting."""
|
||||
|
||||
def __init__(self, base_url: str, name: str, rate_limit: float = 0.0):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.name = name
|
||||
self.rate_limit = rate_limit
|
||||
self._last_request = 0.0
|
||||
self.session = requests.Session()
|
||||
self.session.headers["User-Agent"] = "brk-compat-test/1.0"
|
||||
|
||||
def _wait(self):
|
||||
if self.rate_limit > 0:
|
||||
elapsed = time.monotonic() - self._last_request
|
||||
if elapsed < self.rate_limit:
|
||||
time.sleep(self.rate_limit - elapsed)
|
||||
self._last_request = time.monotonic()
|
||||
|
||||
def get(self, path: str, params=None, timeout: int = 30) -> requests.Response:
|
||||
self._wait()
|
||||
url = f"{self.base_url}{path}"
|
||||
for attempt in range(3):
|
||||
resp = self.session.get(url, params=params, timeout=timeout)
|
||||
if resp.status_code == 429:
|
||||
wait = int(resp.headers.get("Retry-After", 5))
|
||||
time.sleep(wait)
|
||||
continue
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
def get_json(self, path: str, params=None, timeout: int = 30) -> Any:
|
||||
return self.get(path, params=params, timeout=timeout).json()
|
||||
|
||||
def get_text(self, path: str, params=None, timeout: int = 30) -> str:
|
||||
return self.get(path, params=params, timeout=timeout).text
|
||||
|
||||
def get_bytes(self, path: str, params=None, timeout: int = 30) -> bytes:
|
||||
return self.get(path, params=params, timeout=timeout).content
|
||||
|
||||
|
||||
# ── Live data ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
# Absolute heights for well-known eras + relative depths for recent blocks.
|
||||
# Covers: genesis-era, early, mid, post-halving, taproot-era, recent, near-tip.
|
||||
FIXED_HEIGHTS = [100, 100_000, 400_000, 630_000, 800_000]
|
||||
RELATIVE_DEPTHS = [1000, 100, 10]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockData:
|
||||
"""A discovered block with associated txids."""
|
||||
|
||||
height: int
|
||||
hash: str
|
||||
txid: str
|
||||
coinbase_txid: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class LiveData:
|
||||
"""Live blockchain data discovered at session start."""
|
||||
|
||||
tip_height: int
|
||||
tip_hash: str
|
||||
# Multiple blocks at various depths for parametrized tests
|
||||
blocks: list # list[BlockData]
|
||||
# Addresses keyed by scriptpubkey_type
|
||||
addresses: dict # dict[str, str]
|
||||
# Convenience aliases (first block)
|
||||
stable_height: int
|
||||
stable_hash: str
|
||||
stable_block: dict
|
||||
sample_txid: str
|
||||
coinbase_txid: str
|
||||
sample_address: str
|
||||
|
||||
|
||||
# ── Fixtures ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def brk():
|
||||
return ApiClient(BRK_BASE, "brk")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def mempool():
|
||||
return ApiClient(MEMPOOL_BASE, "mempool.space", rate_limit=RATE_LIMIT)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def check_servers(brk, mempool):
|
||||
"""Fail fast if either server is unreachable."""
|
||||
try:
|
||||
brk.get("/api/blocks/tip/height")
|
||||
except Exception as e:
|
||||
pytest.exit(f"brk server not reachable at {brk.base_url}: {e}")
|
||||
try:
|
||||
mempool.get("/api/blocks/tip/height")
|
||||
except Exception as e:
|
||||
pytest.exit(f"mempool.space not reachable at {mempool.base_url}: {e}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def live(mempool) -> LiveData:
|
||||
"""Discover live blockchain data for all tests.
|
||||
|
||||
Fetches blocks at several depths and extracts txids + addresses of
|
||||
different types so parametrized tests hit varied real data.
|
||||
"""
|
||||
tip_height = int(mempool.get_text("/api/blocks/tip/height"))
|
||||
tip_hash = mempool.get_text("/api/blocks/tip/hash")
|
||||
|
||||
heights = FIXED_HEIGHTS + [tip_height - d for d in RELATIVE_DEPTHS]
|
||||
heights.sort()
|
||||
|
||||
blocks: list[BlockData] = []
|
||||
addresses: dict[str, str] = {}
|
||||
|
||||
for h in heights:
|
||||
bh = mempool.get_text(f"/api/block-height/{h}")
|
||||
txids = mempool.get_json(f"/api/block/{bh}/txids")
|
||||
coinbase = txids[0]
|
||||
sample = txids[min(1, len(txids) - 1)]
|
||||
blocks.append(BlockData(height=h, hash=bh, txid=sample, coinbase_txid=coinbase))
|
||||
|
||||
# Collect addresses of different types from non-coinbase outputs
|
||||
if len(addresses) < 8:
|
||||
tx = mempool.get_json(f"/api/tx/{sample}")
|
||||
for vout in tx.get("vout", []):
|
||||
atype = vout.get("scriptpubkey_type")
|
||||
addr = vout.get("scriptpubkey_address")
|
||||
if addr and atype and atype not in addresses:
|
||||
addresses[atype] = addr
|
||||
|
||||
stable = blocks[0]
|
||||
stable_block = mempool.get_json(f"/api/block/{stable.hash}")
|
||||
sample_address = next(iter(addresses.values()), "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
|
||||
|
||||
data = LiveData(
|
||||
tip_height=tip_height,
|
||||
tip_hash=tip_hash,
|
||||
blocks=blocks,
|
||||
addresses=addresses,
|
||||
stable_height=stable.height,
|
||||
stable_hash=stable.hash,
|
||||
stable_block=stable_block,
|
||||
sample_txid=stable.txid,
|
||||
coinbase_txid=stable.coinbase_txid,
|
||||
sample_address=sample_address,
|
||||
)
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(f" LIVE TEST DATA (from {MEMPOOL_BASE})")
|
||||
print(f"{'='*70}")
|
||||
print(f" tip {data.tip_height} {data.tip_hash[:20]}…")
|
||||
for i, b in enumerate(blocks):
|
||||
print(f" block[{i}] {b.height} {b.hash[:20]}… tx={b.txid[:16]}…")
|
||||
for atype, addr in addresses.items():
|
||||
print(f" addr {atype:12s} {addr}")
|
||||
print(f"{'='*70}\n")
|
||||
return data
|
||||
|
||||
|
||||
# ── Display helpers ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def show(method: str, path: str, brk_data: Any, mem_data: Any, max_lines: int = 20):
|
||||
"""Print both responses so the runner can see what was fetched."""
|
||||
print(f"\n{'─'*70}")
|
||||
print(f" {method} {path}")
|
||||
print(f"{'─'*70}")
|
||||
for label, data in [("mempool.space", mem_data), ("brk", brk_data)]:
|
||||
print(f"\n [{label}]")
|
||||
if isinstance(data, (dict, list)):
|
||||
text = json.dumps(data, indent=2)
|
||||
elif isinstance(data, bytes):
|
||||
text = f"<{len(data)} bytes>"
|
||||
else:
|
||||
text = str(data)
|
||||
lines = text.split("\n")
|
||||
for line in lines[:max_lines]:
|
||||
print(f" {line}")
|
||||
if len(lines) > max_lines:
|
||||
print(f" … ({len(lines) - max_lines} more lines)")
|
||||
|
||||
|
||||
# ── Comparison helpers ────────────────────────────────────────────────
|
||||
|
||||
# Keys that brk is intentionally not implementing (mempool.space-specific features).
|
||||
# Everything else that mempool.space returns MUST be present in brk.
|
||||
ALLOWED_MISSING = {
|
||||
"matchRate", "expectedFees", "expectedWeight",
|
||||
# brk only tracks USD — non-USD currencies and exchange rates are intentionally absent
|
||||
"EUR", "GBP", "CAD", "CHF", "AUD", "JPY",
|
||||
"USDEUR", "USDGBP", "USDCAD", "USDCHF", "USDAUD", "USDJPY",
|
||||
# brk doesn't compute block health scores
|
||||
"avgBlockHealth",
|
||||
# brk doesn't compute block similarity/template matching
|
||||
"similarity",
|
||||
# brk doesn't compute fee delta or match rate per pool
|
||||
"avgFeeDelta", "avgMatchRate",
|
||||
}
|
||||
|
||||
# Coinbase transactions use vout=65535 (u16::MAX) in brk vs 4294967295 (u32::MAX)
|
||||
# in mempool.space. This is an intentional representation difference.
|
||||
COINBASE_VOUT_BRK = 65535
|
||||
COINBASE_VOUT_MEMPOOL = 4294967295
|
||||
|
||||
|
||||
def assert_same_structure(brk_data: Any, mem_data: Any, path: str = "root"):
|
||||
"""brk must have every key mempool.space has (extra brk keys are fine).
|
||||
|
||||
Recurses into nested dicts; for arrays, compares the first element.
|
||||
int/float are treated as equivalent; None is compatible with anything.
|
||||
"""
|
||||
if isinstance(mem_data, dict):
|
||||
assert isinstance(brk_data, dict), (
|
||||
f"Expected dict at {path}, got {type(brk_data).__name__}"
|
||||
)
|
||||
brk_keys = set(brk_data.keys())
|
||||
mem_keys = set(mem_data.keys())
|
||||
missing = mem_keys - brk_keys - ALLOWED_MISSING
|
||||
assert not missing, f"brk missing keys at {path}: {missing}"
|
||||
for key in brk_keys & mem_keys:
|
||||
assert_same_structure(brk_data[key], mem_data[key], f"{path}.{key}")
|
||||
elif isinstance(mem_data, list):
|
||||
assert isinstance(brk_data, list), (
|
||||
f"Expected list at {path}, got {type(brk_data).__name__}"
|
||||
)
|
||||
if mem_data and brk_data:
|
||||
assert_same_structure(brk_data[0], mem_data[0], f"{path}[0]")
|
||||
else:
|
||||
if mem_data is None or brk_data is None:
|
||||
return
|
||||
bt = type(brk_data).__name__
|
||||
mt = type(mem_data).__name__
|
||||
if {bt, mt} <= {"int", "float"}:
|
||||
return
|
||||
# int/str are compatible when the string is a numeric literal
|
||||
# (mempool.space serializes large numbers as strings)
|
||||
if {bt, mt} == {"int", "str"}:
|
||||
return
|
||||
assert bt == mt, (
|
||||
f"Type mismatch at {path}: brk={bt}({brk_data!r}) "
|
||||
f"vs mempool={mt}({mem_data!r})"
|
||||
)
|
||||
|
||||
|
||||
def assert_same_values(
|
||||
brk_data: Any,
|
||||
mem_data: Any,
|
||||
path: str = "root",
|
||||
exclude: Optional[Set[str]] = None,
|
||||
):
|
||||
"""Both responses must have identical values.
|
||||
|
||||
Floats are compared with relative tolerance 1e-4.
|
||||
Pass ``exclude`` to skip keys that are expected to differ.
|
||||
"""
|
||||
exclude = exclude or set()
|
||||
|
||||
if isinstance(mem_data, dict):
|
||||
assert isinstance(brk_data, dict), (
|
||||
f"Expected dict at {path}, got {type(brk_data).__name__}"
|
||||
)
|
||||
# brk must have every mempool key; extra brk keys are fine
|
||||
mem_keys = set(mem_data.keys())
|
||||
for key in mem_keys - exclude - ALLOWED_MISSING:
|
||||
assert key in brk_data, f"brk missing '{key}' at {path}"
|
||||
assert_same_values(brk_data[key], mem_data[key], f"{path}.{key}", exclude)
|
||||
elif isinstance(mem_data, list):
|
||||
assert isinstance(brk_data, list), (
|
||||
f"Expected list at {path}, got {type(brk_data).__name__}"
|
||||
)
|
||||
assert len(brk_data) == len(mem_data), (
|
||||
f"Length mismatch at {path}: brk={len(brk_data)} vs mempool={len(mem_data)}"
|
||||
)
|
||||
for i, (b, m) in enumerate(zip(brk_data, mem_data)):
|
||||
assert_same_values(b, m, f"{path}[{i}]", exclude)
|
||||
elif mem_data is None:
|
||||
# mempool returns null, brk computes a value — that's fine
|
||||
return
|
||||
elif isinstance(mem_data, float) or isinstance(brk_data, float):
|
||||
if brk_data is None:
|
||||
return
|
||||
assert float(brk_data) == pytest.approx(
|
||||
float(mem_data), rel=1e-4, abs=1e-6
|
||||
), f"Float mismatch at {path}: brk={brk_data} vs mempool={mem_data}"
|
||||
else:
|
||||
# Coinbase vout: brk uses u16::MAX, mempool uses u32::MAX — both valid
|
||||
if (
|
||||
brk_data == COINBASE_VOUT_BRK
|
||||
and mem_data == COINBASE_VOUT_MEMPOOL
|
||||
):
|
||||
return
|
||||
assert brk_data == mem_data, (
|
||||
f"Value mismatch at {path}: brk={brk_data!r} vs mempool={mem_data!r}"
|
||||
)
|
||||
11
scripts/mempool_compat/pyproject.toml
Normal file
11
scripts/mempool_compat/pyproject.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "mempool-compat"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"pytest>=7.0",
|
||||
"requests>=2.28",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["."]
|
||||
159
scripts/mempool_compat/test_addresses.py
Normal file
159
scripts/mempool_compat/test_addresses.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Address endpoint compatibility tests — parametrized across address types.
|
||||
|
||||
Endpoints covered:
|
||||
GET /api/address/{address}
|
||||
GET /api/address/{address}/txs
|
||||
GET /api/address/{address}/txs/chain
|
||||
GET /api/address/{address}/txs/mempool
|
||||
GET /api/address/{address}/utxo
|
||||
GET /api/v1/validate-address/{address}
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import show, assert_same_structure, assert_same_values
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # P2PKH — early block reward
|
||||
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # P2SH
|
||||
], ids=["p2pkh", "p2sh"])
|
||||
def static_addr(request):
|
||||
"""Well-known addresses that always exist."""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def live_addrs(live):
|
||||
"""All dynamically discovered address types."""
|
||||
return list(live.addresses.items())
|
||||
|
||||
|
||||
# ── /api/address/{address} ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_address_info_static(brk, mempool, static_addr):
|
||||
"""Address stats structure must match for well-known addresses."""
|
||||
path = f"/api/address/{static_addr}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert b["address"] == m["address"]
|
||||
|
||||
|
||||
def test_address_info_discovered(brk, mempool, live_addrs):
|
||||
"""Address stats structure must match for each discovered type."""
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert b["address"] == m["address"]
|
||||
|
||||
|
||||
def test_address_chain_stats_close(brk, mempool, live_addrs):
|
||||
"""Chain stats values must be close for each discovered address."""
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}"
|
||||
b = brk.get_json(path)["chain_stats"]
|
||||
m = mempool.get_json(path)["chain_stats"]
|
||||
show("GET", f"{path} [chain_stats, {atype}]", b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert abs(b["tx_count"] - m["tx_count"]) <= 5, (
|
||||
f"{atype} tx_count: brk={b['tx_count']} vs mempool={m['tx_count']}"
|
||||
)
|
||||
|
||||
|
||||
# ── /api/address/{address}/txs ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_address_txs(brk, mempool, static_addr):
|
||||
"""Address transaction list structure must match."""
|
||||
path = f"/api/address/{static_addr}/txs"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
# ── /api/address/{address}/txs/chain ─────────────────────────────────
|
||||
|
||||
|
||||
def test_address_txs_chain(brk, mempool, static_addr):
|
||||
"""Confirmed-only tx list structure must match."""
|
||||
path = f"/api/address/{static_addr}/txs/chain"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
# ── /api/address/{address}/txs/mempool ────────────────────────────────
|
||||
|
||||
|
||||
def test_address_txs_mempool(brk, mempool, live):
|
||||
"""Mempool tx list must be an array (contents are volatile)."""
|
||||
path = f"/api/address/{live.sample_address}/txs/mempool"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
|
||||
|
||||
# ── /api/address/{address}/utxo ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_address_utxo(brk, mempool, static_addr):
|
||||
"""UTXO list must match — same txids, values, and statuses."""
|
||||
path = f"/api/address/{static_addr}/utxo"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} utxos)", f"({len(m)} utxos)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
# Sort by txid+vout for stable comparison
|
||||
key = lambda u: (u.get("txid", ""), u.get("vout", 0))
|
||||
b_sorted = sorted(b, key=key)
|
||||
m_sorted = sorted(m, key=key)
|
||||
assert_same_values(b_sorted, m_sorted)
|
||||
|
||||
|
||||
# ── /api/v1/validate-address/{address} ───────────────────────────────
|
||||
|
||||
|
||||
def test_validate_address_discovered(brk, mempool, live_addrs):
|
||||
"""Validation of each discovered address type must match exactly."""
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/v1/validate-address/{addr}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", b, m)
|
||||
assert_same_values(b, m)
|
||||
assert b["isvalid"] is True
|
||||
|
||||
|
||||
def test_validate_address_p2pkh(brk, mempool):
|
||||
"""Satoshi's P2PKH address must validate identically."""
|
||||
path = "/api/v1/validate-address/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
assert b["isvalid"] is True
|
||||
|
||||
|
||||
def test_validate_address_invalid(brk, mempool):
|
||||
"""Invalid address must produce the same rejection structure."""
|
||||
path = "/api/v1/validate-address/notanaddress123"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert b["isvalid"] is False
|
||||
assert m["isvalid"] is False
|
||||
assert_same_structure(b, m)
|
||||
265
scripts/mempool_compat/test_blocks.py
Normal file
265
scripts/mempool_compat/test_blocks.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
Block endpoint compatibility tests — parametrized across blockchain eras.
|
||||
|
||||
Endpoints covered:
|
||||
GET /api/block/{hash}
|
||||
GET /api/v1/block/{hash} (with extras)
|
||||
GET /api/block/{hash}/header text/plain
|
||||
GET /api/block/{hash}/status
|
||||
GET /api/block/{hash}/txids
|
||||
GET /api/block/{hash}/txs
|
||||
GET /api/block/{hash}/txs/{start}
|
||||
GET /api/block/{hash}/txid/{index} text/plain
|
||||
GET /api/block-height/{height} text/plain
|
||||
GET /api/blocks
|
||||
GET /api/blocks/{height}
|
||||
GET /api/v1/blocks
|
||||
GET /api/v1/blocks/{height}
|
||||
GET /api/blocks/tip/height text/plain
|
||||
GET /api/blocks/tip/hash text/plain
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import show, assert_same_structure, assert_same_values
|
||||
|
||||
|
||||
def _block_ids(live):
|
||||
return [f"h{b.height}" for b in live.blocks]
|
||||
|
||||
|
||||
def _bi(request, live):
|
||||
"""Resolve parametrized block index, skip if out of range."""
|
||||
i = request.param
|
||||
if i >= len(live.blocks):
|
||||
pytest.skip("block not discovered")
|
||||
return live.blocks[i]
|
||||
|
||||
|
||||
@pytest.fixture(params=range(8), ids=[
|
||||
"h100", "h100k", "h400k", "h630k", "h800k", "recent1k", "recent100", "recent10",
|
||||
])
|
||||
def block(request, live):
|
||||
i = request.param
|
||||
if i >= len(live.blocks):
|
||||
pytest.skip("block not discovered")
|
||||
return live.blocks[i]
|
||||
|
||||
|
||||
# ── /api/block/{hash} ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_by_hash(brk, mempool, block):
|
||||
"""Confirmed block info must be identical."""
|
||||
path = f"/api/block/{block.hash}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/block/{hash} ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_v1_extras_all_values(brk, mempool, block):
|
||||
"""Every shared extras field must match — exposes computation differences."""
|
||||
path = f"/api/v1/block/{block.hash}"
|
||||
b = brk.get_json(path)["extras"]
|
||||
m = mempool.get_json(path)["extras"]
|
||||
show("GET", f"{path} [extras]", b, m, max_lines=50)
|
||||
assert_same_structure(b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
def test_block_v1_extras_pool(brk, mempool, block):
|
||||
"""Pool identification structure must match."""
|
||||
path = f"/api/v1/block/{block.hash}"
|
||||
bp = brk.get_json(path)["extras"]["pool"]
|
||||
mp = mempool.get_json(path)["extras"]["pool"]
|
||||
show("GET", f"{path} [extras.pool]", bp, mp)
|
||||
assert_same_structure(bp, mp)
|
||||
|
||||
|
||||
# ── /api/block/{hash}/header ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_header(brk, mempool, block):
|
||||
"""80-byte hex block header must be identical."""
|
||||
path = f"/api/block/{block.hash}/header"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b, m)
|
||||
assert len(b) == 160, f"Expected 160 hex chars (80 bytes), got {len(b)}"
|
||||
assert b == m
|
||||
|
||||
|
||||
# ── /api/block/{hash}/status ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_status(brk, mempool, block):
|
||||
"""Block status must be identical for a confirmed block."""
|
||||
path = f"/api/block/{block.hash}/status"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
# ── /api/block/{hash}/txids ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_txids(brk, mempool, block):
|
||||
"""Ordered txid list must be identical."""
|
||||
path = f"/api/block/{block.hash}/txids"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b[:3], m[:3])
|
||||
assert b == m
|
||||
|
||||
|
||||
# ── /api/block/{hash}/txs ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_txs_page0(brk, mempool, block):
|
||||
"""First page of block transactions must match."""
|
||||
path = f"/api/block/{block.hash}/txs"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
|
||||
assert len(b) == len(m), f"Page size: brk={len(b)} vs mempool={len(m)}"
|
||||
if b and m:
|
||||
assert_same_values(b[0], m[0], exclude={"sigops"})
|
||||
|
||||
|
||||
def test_block_txs_start_index(brk, mempool, block):
|
||||
"""Paginated txs from index 25 must match (skip small blocks)."""
|
||||
# Blocks with <26 txs don't have a second page
|
||||
txids = mempool.get_json(f"/api/block/{block.hash}/txids")
|
||||
if len(txids) <= 25:
|
||||
pytest.skip(f"block has only {len(txids)} txs")
|
||||
path = f"/api/block/{block.hash}/txs/25"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
|
||||
assert len(b) == len(m)
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
# ── /api/block/{hash}/txid/{index} ───────────────────────────────────
|
||||
|
||||
|
||||
def test_block_txid_at_index_0(brk, mempool, block):
|
||||
"""Txid at position 0 (coinbase) must match."""
|
||||
path = f"/api/block/{block.hash}/txid/0"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b, m)
|
||||
assert b == m
|
||||
|
||||
|
||||
def test_block_txid_at_index_1(brk, mempool, block):
|
||||
"""Txid at position 1 (first non-coinbase) must match."""
|
||||
txids = mempool.get_json(f"/api/block/{block.hash}/txids")
|
||||
if len(txids) <= 1:
|
||||
pytest.skip("block has only coinbase")
|
||||
path = f"/api/block/{block.hash}/txid/1"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b, m)
|
||||
assert b == m
|
||||
|
||||
|
||||
def test_block_txid_at_last_index(brk, mempool, block):
|
||||
"""Txid at last position must match."""
|
||||
txids = mempool.get_json(f"/api/block/{block.hash}/txids")
|
||||
last = len(txids) - 1
|
||||
path = f"/api/block/{block.hash}/txid/{last}"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b, m)
|
||||
assert b == m
|
||||
|
||||
|
||||
# ── /api/block-height/{height} ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_height_to_hash(brk, mempool, block):
|
||||
"""Block hash at a given height must match."""
|
||||
path = f"/api/block-height/{block.height}"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b, m)
|
||||
assert b == m
|
||||
assert b == block.hash
|
||||
|
||||
|
||||
# ── /api/blocks/{height} ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_blocks_from_height(brk, mempool, block):
|
||||
"""Confirmed blocks from a fixed height must match exactly."""
|
||||
path = f"/api/blocks/{block.height}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert len(b) == len(m)
|
||||
if b and m:
|
||||
assert_same_values(b[0], m[0])
|
||||
|
||||
|
||||
def test_blocks_v1_from_height(brk, mempool, block):
|
||||
"""v1 blocks from a confirmed height — all values must match."""
|
||||
path = f"/api/v1/blocks/{block.height}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert len(b) == len(m)
|
||||
if b and m:
|
||||
assert_same_values(b[0], m[0])
|
||||
|
||||
|
||||
# ── non-parametrized (no block param) ────────────────────────────────
|
||||
|
||||
|
||||
def test_blocks_recent(brk, mempool):
|
||||
"""Recent blocks list must have the same structure."""
|
||||
path = "/api/blocks"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show(
|
||||
"GET", path,
|
||||
f"({len(b)} blocks, {b[-1]['height']}–{b[0]['height']})" if b else "[]",
|
||||
f"({len(m)} blocks, {m[-1]['height']}–{m[0]['height']})" if m else "[]",
|
||||
)
|
||||
assert len(b) > 0
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_blocks_v1_recent(brk, mempool):
|
||||
"""Recent v1 blocks (with extras) must have the same structure."""
|
||||
path = "/api/v1/blocks"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert len(b) > 0
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_blocks_tip_height(brk, mempool):
|
||||
"""Tip heights must be within a few blocks of each other."""
|
||||
path = "/api/blocks/tip/height"
|
||||
b = int(brk.get_text(path))
|
||||
m = int(mempool.get_text(path))
|
||||
show("GET", path, b, m)
|
||||
assert abs(b - m) <= 3, f"Tip heights differ by {abs(b - m)}: brk={b}, mempool={m}"
|
||||
|
||||
|
||||
def test_blocks_tip_hash(brk, mempool):
|
||||
"""Tip hash must be a valid 64-char hex string."""
|
||||
path = "/api/blocks/tip/hash"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b, m)
|
||||
assert len(b) == 64
|
||||
assert len(m) == 64
|
||||
84
scripts/mempool_compat/test_fees.py
Normal file
84
scripts/mempool_compat/test_fees.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Fee endpoint compatibility tests.
|
||||
|
||||
Endpoints covered:
|
||||
GET /api/v1/fees/recommended
|
||||
GET /api/v1/fees/precise
|
||||
GET /api/v1/fees/mempool-blocks
|
||||
"""
|
||||
|
||||
from conftest import show, assert_same_structure
|
||||
|
||||
|
||||
EXPECTED_FEE_KEYS = [
|
||||
"fastestFee", "halfHourFee", "hourFee", "economyFee", "minimumFee",
|
||||
]
|
||||
|
||||
|
||||
# ── /api/v1/fees/recommended ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_fees_recommended(brk, mempool):
|
||||
"""Recommended fees must have the same keys and numeric types."""
|
||||
path = "/api/v1/fees/recommended"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
for key in EXPECTED_FEE_KEYS:
|
||||
assert key in b, f"brk missing '{key}'"
|
||||
assert isinstance(b[key], (int, float)), f"'{key}' is not numeric: {type(b[key])}"
|
||||
|
||||
|
||||
def test_fees_recommended_ordering(brk, mempool):
|
||||
"""Fee tiers must be ordered: fastest >= halfHour >= hour >= economy >= minimum."""
|
||||
path = "/api/v1/fees/recommended"
|
||||
for label, client in [("brk", brk), ("mempool", mempool)]:
|
||||
d = client.get_json(path)
|
||||
assert d["fastestFee"] >= d["halfHourFee"] >= d["hourFee"], (
|
||||
f"{label}: fee ordering violated {d}"
|
||||
)
|
||||
assert d["hourFee"] >= d["economyFee"] >= d["minimumFee"], (
|
||||
f"{label}: fee ordering violated {d}"
|
||||
)
|
||||
|
||||
|
||||
# ── /api/v1/fees/precise ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_fees_precise(brk, mempool):
|
||||
"""Precise fees must have the same structure as recommended."""
|
||||
path = "/api/v1/fees/precise"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
for key in EXPECTED_FEE_KEYS:
|
||||
assert key in b
|
||||
|
||||
|
||||
# ── /api/v1/fees/mempool-blocks ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_fees_mempool_blocks(brk, mempool):
|
||||
"""Projected mempool blocks must have the same element structure."""
|
||||
path = "/api/v1/fees/mempool-blocks"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert len(b) > 0
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
def test_fees_mempool_blocks_fee_range(brk, mempool):
|
||||
"""Each projected block must have a 7-element feeRange."""
|
||||
path = "/api/v1/fees/mempool-blocks"
|
||||
for label, client in [("brk", brk), ("mempool", mempool)]:
|
||||
blocks = client.get_json(path)
|
||||
for i, block in enumerate(blocks[:3]):
|
||||
assert "feeRange" in block, f"{label} block {i} missing feeRange"
|
||||
assert len(block["feeRange"]) == 7, (
|
||||
f"{label} block {i} feeRange has {len(block['feeRange'])} items, expected 7"
|
||||
)
|
||||
122
scripts/mempool_compat/test_general.py
Normal file
122
scripts/mempool_compat/test_general.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
General endpoint compatibility tests.
|
||||
|
||||
Endpoints covered:
|
||||
GET /api/v1/difficulty-adjustment
|
||||
GET /api/v1/prices
|
||||
GET /api/v1/historical-price
|
||||
GET /api/v1/historical-price?timestamp=…
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import show, assert_same_structure
|
||||
|
||||
|
||||
DIFFICULTY_KEYS = [
|
||||
"progressPercent", "difficultyChange", "estimatedRetargetDate",
|
||||
"remainingBlocks", "remainingTime", "previousRetarget",
|
||||
"previousTime", "nextRetargetHeight", "timeAvg",
|
||||
"adjustedTimeAvg", "timeOffset", "expectedBlocks",
|
||||
]
|
||||
|
||||
|
||||
# ── /api/v1/difficulty-adjustment ────────────────────────────────────
|
||||
|
||||
|
||||
def test_difficulty_adjustment(brk, mempool):
|
||||
"""Difficulty adjustment must have the same structure."""
|
||||
path = "/api/v1/difficulty-adjustment"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
for key in DIFFICULTY_KEYS:
|
||||
assert key in b, f"brk missing '{key}'"
|
||||
|
||||
|
||||
def test_difficulty_adjustment_values_sane(brk, mempool):
|
||||
"""Progress must be 0–100 %, remaining blocks must be 0–2016."""
|
||||
path = "/api/v1/difficulty-adjustment"
|
||||
for label, client in [("brk", brk), ("mempool", mempool)]:
|
||||
d = client.get_json(path)
|
||||
assert 0 <= d["progressPercent"] <= 100, (
|
||||
f"{label} progressPercent out of range: {d['progressPercent']}"
|
||||
)
|
||||
assert 0 <= d["remainingBlocks"] <= 2016, (
|
||||
f"{label} remainingBlocks out of range: {d['remainingBlocks']}"
|
||||
)
|
||||
|
||||
|
||||
# ── /api/v1/prices ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_prices(brk, mempool):
|
||||
"""Current price must have the same structure."""
|
||||
path = "/api/v1/prices"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert "USD" in b
|
||||
assert "time" in b
|
||||
|
||||
|
||||
def test_prices_positive(brk, mempool):
|
||||
"""USD price must be a positive number on both servers."""
|
||||
path = "/api/v1/prices"
|
||||
for label, client in [("brk", brk), ("mempool", mempool)]:
|
||||
d = client.get_json(path)
|
||||
assert d["USD"] > 0, f"{label} USD price is not positive: {d['USD']}"
|
||||
|
||||
|
||||
# ── /api/v1/historical-price ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_historical_price(brk, mempool):
|
||||
"""Historical price must have the same structure."""
|
||||
path = "/api/v1/historical-price"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m, max_lines=15)
|
||||
assert_same_structure(b, m)
|
||||
assert "prices" in b
|
||||
assert isinstance(b["prices"], list)
|
||||
|
||||
|
||||
def test_historical_price_at_block_timestamps(brk, mempool, live):
|
||||
"""Historical price at each discovered block's timestamp must match structure."""
|
||||
for block in live.blocks:
|
||||
info = brk.get_json(f"/api/block/{block.hash}")
|
||||
ts = info["timestamp"]
|
||||
path = f"/api/v1/historical-price?timestamp={ts}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert "prices" in b
|
||||
assert len(b["prices"]) > 0
|
||||
|
||||
|
||||
# Well-known timestamps from different eras
|
||||
HISTORICAL_TIMESTAMPS = [
|
||||
1231006505, # genesis block (2009-01-03)
|
||||
1354116278, # block 210000 — first halving (2012-11-28)
|
||||
1468082773, # block 420000 — second halving (2016-07-09)
|
||||
1588788036, # block 630000 — third halving (2020-05-11)
|
||||
1713571767, # block 840000 — fourth halving (2024-04-20)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ts", HISTORICAL_TIMESTAMPS, ids=[
|
||||
"genesis", "halving1", "halving2", "halving3", "halving4",
|
||||
])
|
||||
def test_historical_price_at_era(brk, mempool, ts):
|
||||
"""Historical price at well-known timestamps must match structure."""
|
||||
path = f"/api/v1/historical-price?timestamp={ts}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert "prices" in b
|
||||
assert len(b["prices"]) > 0
|
||||
72
scripts/mempool_compat/test_mempool.py
Normal file
72
scripts/mempool_compat/test_mempool.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Mempool endpoint compatibility tests.
|
||||
|
||||
Endpoints covered:
|
||||
GET /api/mempool
|
||||
GET /api/mempool/txids
|
||||
GET /api/mempool/recent
|
||||
"""
|
||||
|
||||
from conftest import show, assert_same_structure
|
||||
|
||||
|
||||
# ── /api/mempool ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_mempool_info(brk, mempool):
|
||||
"""Mempool stats must have the same keys and types."""
|
||||
path = "/api/mempool"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m, max_lines=15)
|
||||
assert_same_structure(b, m)
|
||||
assert isinstance(b["count"], int)
|
||||
assert isinstance(b["vsize"], int)
|
||||
|
||||
|
||||
def test_mempool_info_positive(brk, mempool):
|
||||
"""Both servers must report a non-empty mempool."""
|
||||
path = "/api/mempool"
|
||||
for label, client in [("brk", brk), ("mempool", mempool)]:
|
||||
d = client.get_json(path)
|
||||
assert d["count"] > 0, f"{label} mempool count is 0"
|
||||
assert d["vsize"] > 0, f"{label} mempool vsize is 0"
|
||||
|
||||
|
||||
# ── /api/mempool/txids ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_mempool_txids(brk, mempool):
|
||||
"""Txid list must be a non-empty array of strings."""
|
||||
path = "/api/mempool/txids"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} txids)", f"({len(m)} txids)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert len(b) > 0, "brk mempool has no txids"
|
||||
assert isinstance(b[0], str) and len(b[0]) == 64
|
||||
|
||||
|
||||
# ── /api/mempool/recent ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_mempool_recent(brk, mempool):
|
||||
"""Recent mempool txs must have the same element structure."""
|
||||
path = "/api/mempool/recent"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert len(b) > 0
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
def test_mempool_recent_fields(brk, mempool):
|
||||
"""Each recent tx must have txid, fee, vsize, value."""
|
||||
path = "/api/mempool/recent"
|
||||
for label, client in [("brk", brk), ("mempool", mempool)]:
|
||||
txs = client.get_json(path)
|
||||
for tx in txs[:3]:
|
||||
for key in ["txid", "fee", "vsize", "value"]:
|
||||
assert key in tx, f"{label} recent tx missing '{key}': {tx}"
|
||||
239
scripts/mempool_compat/test_mining.py
Normal file
239
scripts/mempool_compat/test_mining.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Mining endpoint compatibility tests.
|
||||
|
||||
Endpoints covered:
|
||||
GET /api/v1/mining/pools
|
||||
GET /api/v1/mining/pools/{period}
|
||||
GET /api/v1/mining/pool/{slug}
|
||||
GET /api/v1/mining/pool/{slug}/hashrate
|
||||
GET /api/v1/mining/pool/{slug}/blocks
|
||||
GET /api/v1/mining/pool/{slug}/blocks/{height}
|
||||
GET /api/v1/mining/hashrate/{period}
|
||||
GET /api/v1/mining/hashrate/pools/{period}
|
||||
GET /api/v1/mining/difficulty-adjustments/{period}
|
||||
GET /api/v1/mining/reward-stats/{block_count}
|
||||
GET /api/v1/mining/blocks/fees/{period}
|
||||
GET /api/v1/mining/blocks/rewards/{period}
|
||||
GET /api/v1/mining/blocks/fee-rates/{period}
|
||||
GET /api/v1/mining/blocks/sizes-weights/{period}
|
||||
GET /api/v1/mining/blocks/timestamp/{timestamp}
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import show, assert_same_structure
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pool_slugs(mempool):
|
||||
"""Discover the top 3 active pool slugs from the last week."""
|
||||
data = mempool.get_json("/api/v1/mining/pools/1w")
|
||||
pools = data.get("pools", []) if isinstance(data, dict) else []
|
||||
slugs = [p["slug"] for p in pools if p.get("blockCount", 0) > 0][:3]
|
||||
return slugs or ["foundryusa"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pool_slug(pool_slugs):
|
||||
return pool_slugs[0]
|
||||
|
||||
|
||||
# ── /api/v1/mining/pools ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_mining_pools_list(brk, mempool):
|
||||
"""Pool list must have the same element structure."""
|
||||
path = "/api/v1/mining/pools"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b[:3] if isinstance(b, list) else b, m[:3] if isinstance(m, list) else m)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"])
|
||||
def test_mining_pools_by_period(brk, mempool, period):
|
||||
"""Pool stats for a time period must have the same structure."""
|
||||
path = f"/api/v1/mining/pools/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/pool/{slug} ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_mining_pool_detail(brk, mempool, pool_slugs):
|
||||
"""Pool detail must have the same structure for top pools."""
|
||||
for slug in pool_slugs:
|
||||
path = f"/api/v1/mining/pool/{slug}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_pool_hashrate(brk, mempool, pool_slugs):
|
||||
"""Pool hashrate history must have the same structure for top pools."""
|
||||
for slug in pool_slugs:
|
||||
path = f"/api/v1/mining/pool/{slug}/hashrate"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_pool_blocks(brk, mempool, pool_slugs):
|
||||
"""Recent blocks by pool must have the same element structure."""
|
||||
for slug in pool_slugs:
|
||||
path = f"/api/v1/mining/pool/{slug}/blocks"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
def test_mining_pool_blocks_at_height(brk, mempool, pool_slug, live):
|
||||
"""Pool blocks before various heights must have the same element structure."""
|
||||
for block in live.blocks[::2]: # every other block
|
||||
path = f"/api/v1/mining/pool/{pool_slug}/blocks/{block.height}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
# ── /api/v1/mining/hashrate ──────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y"])
|
||||
def test_mining_hashrate(brk, mempool, period):
|
||||
"""Network hashrate + difficulty must have the same structure."""
|
||||
path = f"/api/v1/mining/hashrate/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/hashrate/pools ────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "1y"])
|
||||
def test_mining_hashrate_pools(brk, mempool, period):
|
||||
"""Per-pool hashrate must have the same structure."""
|
||||
path = f"/api/v1/mining/hashrate/pools/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/difficulty-adjustments ─────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y"])
|
||||
def test_mining_difficulty_adjustments(brk, mempool, period):
|
||||
"""Historical difficulty adjustments must have the same structure."""
|
||||
path = f"/api/v1/mining/difficulty-adjustments/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/reward-stats ──────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("block_count", [10, 100, 500])
|
||||
def test_mining_reward_stats(brk, mempool, block_count):
|
||||
"""Reward stats must have the same structure."""
|
||||
path = f"/api/v1/mining/reward-stats/{block_count}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/blocks/fees ───────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_fees(brk, mempool, period):
|
||||
"""Average block fees must have the same element structure."""
|
||||
path = f"/api/v1/mining/blocks/fees/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/blocks/rewards ────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_rewards(brk, mempool, period):
|
||||
"""Average block rewards must have the same element structure."""
|
||||
path = f"/api/v1/mining/blocks/rewards/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/blocks/fee-rates ──────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_fee_rates(brk, mempool, period):
|
||||
"""Block fee-rate percentiles must have the same element structure."""
|
||||
path = f"/api/v1/mining/blocks/fee-rates/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/blocks/sizes-weights ──────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_sizes_weights(brk, mempool, period):
|
||||
"""Block sizes and weights must have the same structure."""
|
||||
path = f"/api/v1/mining/blocks/sizes-weights/{period}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, _summary(b), _summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/mining/blocks/timestamp ──────────────────────────────────
|
||||
|
||||
|
||||
def test_mining_blocks_timestamp(brk, mempool, live):
|
||||
"""Block lookup by timestamp must have the same structure for various eras."""
|
||||
for block in live.blocks:
|
||||
# Get the block timestamp from brk
|
||||
info = brk.get_json(f"/api/block/{block.hash}")
|
||||
ts = info["timestamp"]
|
||||
path = f"/api/v1/mining/blocks/timestamp/{ts}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _summary(data):
|
||||
"""Short description of a response for the show() call."""
|
||||
if isinstance(data, list):
|
||||
return f"({len(data)} items)"
|
||||
if isinstance(data, dict):
|
||||
return f"(keys: {list(data.keys())})"
|
||||
return str(data)
|
||||
161
scripts/mempool_compat/test_transactions.py
Normal file
161
scripts/mempool_compat/test_transactions.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Transaction endpoint compatibility tests — parametrized across blockchain eras.
|
||||
|
||||
Endpoints covered:
|
||||
GET /api/tx/{txid}
|
||||
GET /api/tx/{txid}/hex text/plain
|
||||
GET /api/tx/{txid}/raw application/octet-stream
|
||||
GET /api/tx/{txid}/status
|
||||
GET /api/tx/{txid}/merkle-proof
|
||||
GET /api/tx/{txid}/merkleblock-proof text/plain
|
||||
GET /api/tx/{txid}/outspend/{vout}
|
||||
GET /api/tx/{txid}/outspends
|
||||
GET /api/v1/cpfp/{txid}
|
||||
GET /api/v1/transaction-times
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import show, assert_same_structure, assert_same_values
|
||||
|
||||
|
||||
@pytest.fixture(params=range(8), ids=[
|
||||
"h100", "h100k", "h400k", "h630k", "h800k", "recent1k", "recent100", "recent10",
|
||||
])
|
||||
def block(request, live):
|
||||
i = request.param
|
||||
if i >= len(live.blocks):
|
||||
pytest.skip("block not discovered")
|
||||
return live.blocks[i]
|
||||
|
||||
|
||||
# ── /api/tx/{txid} ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_by_id(brk, mempool, block):
|
||||
"""Full transaction data must match for a confirmed tx."""
|
||||
path = f"/api/tx/{block.txid}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m, exclude={"sigops"})
|
||||
|
||||
|
||||
def test_tx_coinbase(brk, mempool, block):
|
||||
"""Coinbase transaction must match."""
|
||||
path = f"/api/tx/{block.coinbase_txid}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m, exclude={"sigops"})
|
||||
|
||||
|
||||
# ── /api/tx/{txid}/hex ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_hex(brk, mempool, block):
|
||||
"""Raw transaction hex must be identical."""
|
||||
path = f"/api/tx/{block.txid}/hex"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b[:80] + "…", m[:80] + "…")
|
||||
assert b == m
|
||||
|
||||
|
||||
# ── /api/tx/{txid}/raw ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_raw(brk, mempool, block):
|
||||
"""Raw transaction bytes must be identical."""
|
||||
path = f"/api/tx/{block.txid}/raw"
|
||||
b = brk.get_bytes(path)
|
||||
m = mempool.get_bytes(path)
|
||||
show("GET", path, f"<{len(b)} bytes>", f"<{len(m)} bytes>")
|
||||
assert b == m
|
||||
|
||||
|
||||
# ── /api/tx/{txid}/status ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_status(brk, mempool, block):
|
||||
"""Confirmation status must match for a confirmed tx."""
|
||||
path = f"/api/tx/{block.txid}/status"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
# ── /api/tx/{txid}/merkle-proof ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_merkle_proof(brk, mempool, block):
|
||||
"""Merkle inclusion proof must match."""
|
||||
path = f"/api/tx/{block.txid}/merkle-proof"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
# ── /api/tx/{txid}/merkleblock-proof ─────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_merkleblock_proof(brk, mempool, block):
|
||||
"""BIP37 merkleblock proof hex must be identical."""
|
||||
path = f"/api/tx/{block.txid}/merkleblock-proof"
|
||||
b = brk.get_text(path)
|
||||
m = mempool.get_text(path)
|
||||
show("GET", path, b[:80] + "…", m[:80] + "…")
|
||||
assert b == m
|
||||
|
||||
|
||||
# ── /api/tx/{txid}/outspend/{vout} ───────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_outspend(brk, mempool, block):
|
||||
"""Spending status of output 0 must match exactly."""
|
||||
path = f"/api/tx/{block.txid}/outspend/0"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
# ── /api/tx/{txid}/outspends ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_tx_outspends(brk, mempool, block):
|
||||
"""Spending status of all outputs must match exactly."""
|
||||
path = f"/api/tx/{block.txid}/outspends"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/cpfp/{txid} ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_cpfp(brk, mempool, block):
|
||||
"""CPFP info structure must match for a confirmed tx."""
|
||||
path = f"/api/v1/cpfp/{block.txid}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
# ── /api/v1/transaction-times ────────────────────────────────────────
|
||||
|
||||
|
||||
def test_transaction_times(brk, mempool, live):
|
||||
"""First-seen timestamps array must have the same length."""
|
||||
txids = [b.txid for b in live.blocks[:3]]
|
||||
params = [("txId[]", t) for t in txids]
|
||||
path = "/api/v1/transaction-times"
|
||||
b = brk.get_json(path, params=params)
|
||||
m = mempool.get_json(path, params=params)
|
||||
show("GET", f"{path}?txId[]={{{len(txids)} txids}}", b, m)
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert len(b) == len(m) == len(txids)
|
||||
385
scripts/mempool_compat/uv.lock
generated
Normal file
385
scripts/mempool_compat/uv.lock
generated
Normal file
@@ -0,0 +1,385 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.9"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.2.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mempool-compat"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "requests", version = "2.33.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "pytest", specifier = ">=7.0" },
|
||||
{ name = "requests", specifier = ">=2.28" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.10'" },
|
||||
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "packaging", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pluggy", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pygments", marker = "python_full_version < '3.10'" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pluggy", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pygments", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "certifi", marker = "python_full_version < '3.10'" },
|
||||
{ name = "charset-normalizer", marker = "python_full_version < '3.10'" },
|
||||
{ name = "idna", marker = "python_full_version < '3.10'" },
|
||||
{ name = "urllib3", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.33.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "certifi", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "charset-normalizer", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "idna", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "urllib3", marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
@@ -55,9 +55,12 @@
|
||||
* @typedef {Brk._0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdZscorePattern} Ratio1ySdPattern
|
||||
* @typedef {Brk.Dollars} Dollars
|
||||
* @typedef {Brk.BlockInfo} BlockInfo
|
||||
* @typedef {Brk.Height} Height
|
||||
* @typedef {Brk.BlockHash} BlockHash
|
||||
* @typedef {Brk.BlockInfoV1} BlockInfoV1
|
||||
* @typedef {Brk.Transaction} Transaction
|
||||
* @typedef {Brk.Txid} Txid
|
||||
* @typedef {Brk.TxIndex} TxIndex
|
||||
* @typedef {Brk.AddrStats} AddrStats
|
||||
* @typedef {Brk.TxIn} TxIn
|
||||
* @typedef {Brk.TxOut} TxOut
|
||||
|
||||
@@ -56,10 +56,16 @@ export function initChain(parent, callbacks) {
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {string} hash */
|
||||
export function findCube(hash) {
|
||||
/** @param {BlockHash | Height | null} [hashOrHeight] */
|
||||
function findCube(hashOrHeight) {
|
||||
if (hashOrHeight == null) {
|
||||
return reachedTip && newestHeight >= 0
|
||||
? /** @type {HTMLDivElement | null} */ (blocksEl.lastElementChild)
|
||||
: null;
|
||||
}
|
||||
const attr = typeof hashOrHeight === "number" ? "height" : "hash";
|
||||
return /** @type {HTMLDivElement | null} */ (
|
||||
blocksEl.querySelector(`[data-hash="${hash}"]`)
|
||||
blocksEl.querySelector(`[data-${attr}="${hashOrHeight}"]`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +92,7 @@ export function selectCube(cube, { scroll, silent } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
function clear() {
|
||||
newestHeight = -1;
|
||||
oldestHeight = Infinity;
|
||||
loadingOlder = false;
|
||||
@@ -126,12 +132,13 @@ function appendNewerBlocks(blocks) {
|
||||
}
|
||||
|
||||
/** @param {number | null} [height] @returns {Promise<BlockHash>} */
|
||||
export async function loadInitial(height) {
|
||||
async function loadInitial(height) {
|
||||
const blocks =
|
||||
height != null
|
||||
? await brk.getBlocksV1FromHeight(height)
|
||||
: await brk.getBlocksV1();
|
||||
|
||||
clear();
|
||||
for (const b of blocks) blocksEl.prepend(createBlockCube(b));
|
||||
newestHeight = blocks[0].height;
|
||||
oldestHeight = blocks[blocks.length - 1].height;
|
||||
@@ -141,6 +148,39 @@ export async function loadInitial(height) {
|
||||
return blocks[0].id;
|
||||
}
|
||||
|
||||
/** @param {BlockHash | Height | null} [hashOrHeight] @returns {Promise<Height | null>} */
|
||||
async function resolveHeight(hashOrHeight) {
|
||||
if (typeof hashOrHeight === "number") return hashOrHeight;
|
||||
if (typeof hashOrHeight === "string") {
|
||||
const cached = blocksByHash.get(hashOrHeight);
|
||||
if (cached) return cached.height;
|
||||
const block = await brk.getBlockV1(hashOrHeight);
|
||||
blocksByHash.set(hashOrHeight, block);
|
||||
return block.height;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param {BlockHash | Height | string | null} [hashOrHeight] @param {{ silent?: boolean }} [options] */
|
||||
export async function goToCube(hashOrHeight, { silent } = {}) {
|
||||
if (typeof hashOrHeight === "string" && /^\d+$/.test(hashOrHeight)) {
|
||||
hashOrHeight = Number(hashOrHeight);
|
||||
}
|
||||
let cube = findCube(hashOrHeight);
|
||||
if (cube) {
|
||||
selectCube(cube, { scroll: "smooth", silent });
|
||||
return;
|
||||
}
|
||||
let startHash;
|
||||
try {
|
||||
const height = await resolveHeight(hashOrHeight);
|
||||
startHash = await loadInitial(height);
|
||||
} catch (e) {
|
||||
try { startHash = await loadInitial(null); } catch (_) { return; }
|
||||
}
|
||||
selectCube(/** @type {HTMLDivElement} */ (findCube(startHash)), { scroll: "instant", silent });
|
||||
}
|
||||
|
||||
export async function poll() {
|
||||
if (newestHeight === -1 || !reachedTip) return;
|
||||
try {
|
||||
@@ -185,6 +225,7 @@ function createBlockCube(block) {
|
||||
createCube();
|
||||
|
||||
cubeElement.dataset.hash = block.id;
|
||||
cubeElement.dataset.height = String(block.height);
|
||||
blocksByHash.set(block.id, block);
|
||||
cubeElement.addEventListener("click", () => onCubeClick(cubeElement));
|
||||
|
||||
|
||||
@@ -3,12 +3,10 @@ import { brk } from "../utils/client.js";
|
||||
import { createMapCache } from "../utils/cache.js";
|
||||
import {
|
||||
initChain,
|
||||
loadInitial,
|
||||
goToCube,
|
||||
poll,
|
||||
selectCube,
|
||||
deselectCube,
|
||||
findCube,
|
||||
clear as clearChain,
|
||||
} from "./chain.js";
|
||||
import {
|
||||
initBlockDetails,
|
||||
@@ -38,10 +36,12 @@ function pathSegments() {
|
||||
/** @type {number | undefined} */ let pollInterval;
|
||||
let navController = new AbortController();
|
||||
const txCache = createMapCache(50);
|
||||
let lastLoadedUrl = "";
|
||||
|
||||
function navigate() {
|
||||
navController.abort();
|
||||
navController = new AbortController();
|
||||
lastLoadedUrl = window.location.pathname;
|
||||
return navController.signal;
|
||||
}
|
||||
|
||||
@@ -60,27 +60,27 @@ function handleLinkClick(e) {
|
||||
const m = a.pathname.match(/^\/(block|tx|address)\/(.+)/);
|
||||
if (!m) return;
|
||||
e.preventDefault();
|
||||
history.pushState(null, "", a.href);
|
||||
if (m[1] === "block") {
|
||||
navigateToBlock(m[2]);
|
||||
} else if (m[1] === "tx") {
|
||||
history.pushState(null, "", a.href);
|
||||
navigateToTx(m[2]);
|
||||
} else {
|
||||
history.pushState(null, "", a.href);
|
||||
navigateToAddr(m[2]);
|
||||
}
|
||||
}
|
||||
|
||||
export function init() {
|
||||
/** @param {{ onChange: (cb: (option: Option) => void) => void }} selected */
|
||||
export function init(selected) {
|
||||
initChain(explorerElement, {
|
||||
onSelect: (block) => {
|
||||
updateBlock(block);
|
||||
showPanel("block");
|
||||
},
|
||||
onCubeClick: (cube) => {
|
||||
navigate();
|
||||
const hash = cube.dataset.hash;
|
||||
if (hash) history.pushState(null, "", `/block/${hash}`);
|
||||
navigate();
|
||||
selectCube(cube);
|
||||
},
|
||||
});
|
||||
@@ -101,15 +101,12 @@ export function init() {
|
||||
if (!document.hidden && !explorerElement.hidden) poll();
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
const [kind, value] = pathSegments();
|
||||
if (kind === "block" && value) navigateToBlock(value, false);
|
||||
else if (kind === "tx" && value) navigateToTx(value);
|
||||
else if (kind === "address" && value) navigateToAddr(value);
|
||||
else showPanel("block");
|
||||
selected.onChange((option) => {
|
||||
if (option.kind === "explorer") {
|
||||
const url = window.location.pathname;
|
||||
if (url !== lastLoadedUrl) load();
|
||||
}
|
||||
});
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
@@ -126,98 +123,77 @@ function stopPolling() {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const signal = navigate();
|
||||
try {
|
||||
const [kind, value] = pathSegments();
|
||||
|
||||
if (kind === "tx" && value) {
|
||||
const tx = txCache.get(value) ?? (await brk.getTx(value));
|
||||
txCache.set(value, tx);
|
||||
const startHash = await loadInitial(tx.status?.blockHeight ?? null);
|
||||
const cube = tx.status?.blockHash ? findCube(tx.status.blockHash) : findCube(startHash);
|
||||
if (cube) selectCube(cube, { silent: true });
|
||||
const txid = await resolveTxid(value, { signal });
|
||||
if (signal.aborted) return;
|
||||
const tx = txCache.get(txid) ?? (await brk.getTx(txid, { signal }));
|
||||
if (signal.aborted) return;
|
||||
txCache.set(txid, tx);
|
||||
await goToCube(tx.status?.blockHash ?? tx.status?.blockHeight ?? null, { silent: true });
|
||||
updateTx(tx);
|
||||
showPanel("tx");
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind === "address" && value) {
|
||||
const startHash = await loadInitial(null);
|
||||
const cube = findCube(startHash);
|
||||
if (cube) selectCube(cube, { silent: true });
|
||||
await goToCube(null, { silent: true });
|
||||
navigateToAddr(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const height =
|
||||
kind === "block" && value
|
||||
? /^\d+$/.test(value)
|
||||
? Number(value)
|
||||
: (await brk.getBlockV1(value)).height
|
||||
: null;
|
||||
const startHash = await loadInitial(height);
|
||||
const cube = findCube(startHash);
|
||||
if (cube) selectCube(cube, { scroll: "instant" });
|
||||
await goToCube(kind === "block" ? value : null);
|
||||
} catch (e) {
|
||||
if (signal.aborted) return;
|
||||
console.error("explorer load:", e);
|
||||
await goToCube();
|
||||
showPanel("block");
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} hash @param {boolean} [pushUrl] */
|
||||
async function navigateToBlock(hash, pushUrl = true) {
|
||||
if (pushUrl) history.pushState(null, "", `/block/${hash}`);
|
||||
const existing = findCube(hash);
|
||||
if (existing) {
|
||||
navigate();
|
||||
selectCube(existing, { scroll: "smooth" });
|
||||
return;
|
||||
}
|
||||
/** @param {string} hashOrHeight */
|
||||
async function navigateToBlock(hashOrHeight) {
|
||||
const signal = navigate();
|
||||
try {
|
||||
clearChain();
|
||||
const height = /^\d+$/.test(hash)
|
||||
? Number(hash)
|
||||
: (await brk.getBlockV1(hash, { signal })).height;
|
||||
if (signal.aborted) return;
|
||||
const startHash = await loadInitial(height);
|
||||
if (signal.aborted) return;
|
||||
const cube = findCube(hash) ?? findCube(startHash);
|
||||
if (cube) selectCube(cube);
|
||||
} catch (e) {
|
||||
if (!signal.aborted) console.error("explorer block:", e);
|
||||
}
|
||||
await goToCube(hashOrHeight);
|
||||
if (!signal.aborted) showPanel("block");
|
||||
}
|
||||
|
||||
/** @param {string} txid */
|
||||
async function navigateToTx(txid) {
|
||||
/** @param {Txid | TxIndex} value @param {{ signal?: AbortSignal }} [options] */
|
||||
async function resolveTxid(value, { signal } = {}) {
|
||||
return typeof value === "number" || /^\d+$/.test(value)
|
||||
? await brk.getTxByIndex(Number(value), { signal })
|
||||
: value;
|
||||
}
|
||||
|
||||
/** @param {Txid | TxIndex} txidOrIndex */
|
||||
async function navigateToTx(txidOrIndex) {
|
||||
const signal = navigate();
|
||||
clearTx();
|
||||
showPanel("tx");
|
||||
try {
|
||||
const txid = await resolveTxid(txidOrIndex, { signal });
|
||||
if (signal.aborted) return;
|
||||
const tx = txCache.get(txid) ?? (await brk.getTx(txid, { signal }));
|
||||
if (signal.aborted) return;
|
||||
txCache.set(txid, tx);
|
||||
|
||||
if (tx.status?.blockHash) {
|
||||
let cube = findCube(tx.status.blockHash);
|
||||
if (!cube) {
|
||||
clearChain();
|
||||
const startHash = await loadInitial(tx.status.blockHeight ?? null);
|
||||
if (signal.aborted) return;
|
||||
cube = findCube(tx.status.blockHash) ?? findCube(startHash);
|
||||
}
|
||||
if (cube) selectCube(cube, { scroll: "smooth", silent: true });
|
||||
}
|
||||
|
||||
await goToCube(tx.status?.blockHash ?? tx.status?.blockHeight ?? null, { silent: true });
|
||||
updateTx(tx);
|
||||
} catch (e) {
|
||||
if (!signal.aborted) console.error("explorer tx:", e);
|
||||
if (!signal.aborted) {
|
||||
console.error("explorer tx:", e);
|
||||
await goToCube();
|
||||
showPanel("block");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} address */
|
||||
function navigateToAddr(address) {
|
||||
navigate();
|
||||
const signal = navigate();
|
||||
deselectCube();
|
||||
updateAddr(address, navController.signal);
|
||||
updateAddr(address, signal);
|
||||
showPanel("addr");
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ function initSelected() {
|
||||
element = explorerElement;
|
||||
|
||||
if (firstTimeLoadingExplorer) {
|
||||
initExplorer();
|
||||
initExplorer(options.selected);
|
||||
}
|
||||
firstTimeLoadingExplorer = false;
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@ export function initOptions() {
|
||||
highlightedLis.push(li);
|
||||
}
|
||||
}
|
||||
if (!highlightedLis.length) {
|
||||
const li = liByPath.get(stringToId(sel.name));
|
||||
if (li) {
|
||||
li.dataset.highlight = "";
|
||||
highlightedLis.push(li);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selected = {
|
||||
@@ -360,8 +367,9 @@ export function initOptions() {
|
||||
/**
|
||||
* @param {ProcessedNode[]} nodes
|
||||
* @param {HTMLElement} parentEl
|
||||
* @param {boolean} autoOpen
|
||||
*/
|
||||
function buildTreeDOM(nodes, parentEl) {
|
||||
function buildTreeDOM(nodes, parentEl, autoOpen) {
|
||||
const ul = window.document.createElement("ul");
|
||||
|
||||
for (const node of nodes) {
|
||||
@@ -370,8 +378,6 @@ export function initOptions() {
|
||||
|
||||
liByPath.set(node.pathKey, li);
|
||||
|
||||
const onSelectedPath = isOnSelectedPath(node.path);
|
||||
|
||||
if (node.type === "group") {
|
||||
const details = window.document.createElement("details");
|
||||
details.dataset.name = node.serName;
|
||||
@@ -386,16 +392,17 @@ export function initOptions() {
|
||||
summary.append(count);
|
||||
|
||||
let built = false;
|
||||
if (onSelectedPath) {
|
||||
if (autoOpen && isOnSelectedPath(node.path)) {
|
||||
built = true;
|
||||
details.open = true;
|
||||
buildTreeDOM(node.children, details);
|
||||
buildTreeDOM(node.children, details, true);
|
||||
}
|
||||
details.addEventListener("toggle", () => {
|
||||
if (details.open && !built) {
|
||||
built = true;
|
||||
buildTreeDOM(node.children, details);
|
||||
buildTreeDOM(node.children, details, false);
|
||||
}
|
||||
updateHighlight(selected.value);
|
||||
});
|
||||
} else {
|
||||
const element = createOptionElement({
|
||||
@@ -417,7 +424,7 @@ export function initOptions() {
|
||||
function setParent(el) {
|
||||
if (parentEl) return;
|
||||
parentEl = el;
|
||||
buildTreeDOM(processedTree, el);
|
||||
buildTreeDOM(processedTree, el, true);
|
||||
updateHighlight(selected.value);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,44 +18,104 @@ export function init(options) {
|
||||
|
||||
const matcher = new QuickMatch(haystack);
|
||||
|
||||
/** @type {HTMLLIElement | undefined} */
|
||||
let highlighted;
|
||||
|
||||
/** @param {HTMLLIElement} [li] */
|
||||
function setHighlight(li) {
|
||||
if (highlighted) delete highlighted.dataset.highlight;
|
||||
highlighted = li;
|
||||
if (li) li.dataset.highlight = "";
|
||||
}
|
||||
|
||||
function inputEvent() {
|
||||
const needle = /** @type {string} */ (searchInput.value).trim();
|
||||
|
||||
searchResultsElement.scrollTo({ top: 0 });
|
||||
searchResultsElement.innerHTML = "";
|
||||
setHighlight();
|
||||
|
||||
if (!needle.length) {
|
||||
return;
|
||||
}
|
||||
if (!needle.length) return;
|
||||
|
||||
const matches = matcher.matches(needle);
|
||||
|
||||
if (!matches.length) {
|
||||
const indexMatch = needle.match(
|
||||
/^(?:(block|b)|(transaction|tx))?\s*#?\s*(\d+)$/i,
|
||||
);
|
||||
|
||||
if (indexMatch) {
|
||||
const num = indexMatch[3];
|
||||
const entries = indexMatch[1]
|
||||
? [["Block", `/block/${num}`]]
|
||||
: indexMatch[2]
|
||||
? [["Transaction", `/tx/${num}`]]
|
||||
: [
|
||||
["Block", `/block/${num}`],
|
||||
["Transaction", `/tx/${num}`],
|
||||
];
|
||||
for (const [label, href] of entries) {
|
||||
const li = window.document.createElement("li");
|
||||
const a = window.document.createElement("a");
|
||||
a.href = href;
|
||||
a.textContent = `${label} #${num}`;
|
||||
a.title = `${label} #${num}`;
|
||||
if (href === window.location.pathname) setHighlight(li);
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
setHighlight(li);
|
||||
history.pushState(null, "", href);
|
||||
options.resolveUrl();
|
||||
});
|
||||
li.append(a);
|
||||
searchResultsElement.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length) {
|
||||
matches.forEach((title) => {
|
||||
const option = titleToOption.get(title);
|
||||
if (!option) return;
|
||||
|
||||
const li = window.document.createElement("li");
|
||||
searchResultsElement.appendChild(li);
|
||||
|
||||
if (option === options.selected.value) setHighlight(li);
|
||||
|
||||
const element = options.createOptionElement({
|
||||
option,
|
||||
name: option.title,
|
||||
});
|
||||
|
||||
if (element) li.append(element);
|
||||
});
|
||||
}
|
||||
|
||||
if (!searchResultsElement.children.length) {
|
||||
const li = window.document.createElement("li");
|
||||
li.textContent = "No results";
|
||||
li.style.color = "var(--off-color)";
|
||||
searchResultsElement.appendChild(li);
|
||||
return;
|
||||
}
|
||||
|
||||
matches.forEach((title) => {
|
||||
const option = titleToOption.get(title);
|
||||
if (!option) return;
|
||||
|
||||
const li = window.document.createElement("li");
|
||||
searchResultsElement.appendChild(li);
|
||||
|
||||
const element = options.createOptionElement({
|
||||
option,
|
||||
name: option.title,
|
||||
});
|
||||
|
||||
if (element) {
|
||||
li.append(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
options.selected.onChange(() => {
|
||||
const selected = options.selected.value;
|
||||
const href =
|
||||
selected?.kind === "explorer"
|
||||
? window.location.pathname
|
||||
: selected?.path.length
|
||||
? `/${selected.path.join("/")}`
|
||||
: null;
|
||||
if (!href) return setHighlight();
|
||||
for (const li of searchResultsElement.children) {
|
||||
const a = li.querySelector("a");
|
||||
if (a && a.getAttribute("href") === href) {
|
||||
return setHighlight(/** @type {HTMLLIElement} */ (li));
|
||||
}
|
||||
}
|
||||
setHighlight();
|
||||
});
|
||||
|
||||
inputEvent();
|
||||
|
||||
searchInput.addEventListener("input", inputEvent);
|
||||
|
||||
@@ -42,6 +42,7 @@ main {
|
||||
html[data-layout="split"] & {
|
||||
min-width: 100%;
|
||||
max-width: var(--max-main-width);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
> nav,
|
||||
|
||||
@@ -41,29 +41,29 @@ nav {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li[data-highlight] {
|
||||
> details > summary,
|
||||
> a {
|
||||
text-transform: uppercase;
|
||||
color: var(--color);
|
||||
}
|
||||
li[data-highlight] {
|
||||
> details > summary,
|
||||
> a {
|
||||
text-transform: uppercase;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
> details > summary > small {
|
||||
opacity: 1;
|
||||
}
|
||||
> details > summary > small {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> a::after,
|
||||
> details:not([open]) > summary::after {
|
||||
color: var(--orange) !important;
|
||||
content: "";
|
||||
background-color: var(--orange);
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 9999px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-left: 0.375rem;
|
||||
}
|
||||
> a::after,
|
||||
> details:not([open]) > summary::after {
|
||||
color: var(--orange) !important;
|
||||
content: "";
|
||||
background-color: var(--orange);
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 9999px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-left: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
--gap: 0.75;
|
||||
--gap: 0.8;
|
||||
gap: calc(var(--cube) * var(--gap));
|
||||
margin-right: var(--cube);
|
||||
margin-top: calc(var(--cube) * -0.25);
|
||||
|
||||
Reference in New Issue
Block a user