From b24bfdc15cf11355857452547f81117772b4e632 Mon Sep 17 00:00:00 2001 From: nym21 Date: Mon, 27 Apr 2026 11:19:05 +0200 Subject: [PATCH] global: fixes --- .../src/generators/python/types.rs | 5 +- crates/brk_cli/src/config.rs | 60 ++++---- crates/brk_client/src/lib.rs | 2 +- crates/brk_server/src/api/series_legacy.rs | 1 + .../brk_server/src/params/addr_txids_param.rs | 1 + .../brk_server/src/params/timestamp_param.rs | 1 + crates/brk_server/src/params/urpd_params.rs | 1 + crates/brk_types/src/pagination.rs | 1 + crates/brk_types/src/search_query.rs | 1 + modules/brk-client/index.js | 2 +- modules/brk-client/package.json | 2 +- packages/brk_client/brk_client/__init__.py | 140 +++++++++--------- packages/brk_client/pyproject.toml | 2 +- packages/brk_client/uv.lock | 2 +- scripts/cf-purge.sh | 6 +- 15 files changed, 116 insertions(+), 111 deletions(-) diff --git a/crates/brk_bindgen/src/generators/python/types.rs b/crates/brk_bindgen/src/generators/python/types.rs index 5a61a7b57..27782172f 100644 --- a/crates/brk_bindgen/src/generators/python/types.rs +++ b/crates/brk_bindgen/src/generators/python/types.rs @@ -110,8 +110,9 @@ fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec { for (name, schema) in schemas { let mut type_deps = BTreeSet::new(); collect_schema_refs(schema, &mut type_deps); - // Only keep deps that are in our schemas - type_deps.retain(|d| schemas.contains_key(d)); + // Only keep deps that are in our schemas, and drop self-references + // (handled at emit time by quoting via current_type) + type_deps.retain(|d| schemas.contains_key(d) && d != name); deps.insert(name.clone(), type_deps); } diff --git a/crates/brk_cli/src/config.rs b/crates/brk_cli/src/config.rs index 4d8cfe696..342b1fa34 100644 --- a/crates/brk_cli/src/config.rs +++ b/crates/brk_cli/src/config.rs @@ -1,5 +1,5 @@ use std::{ - fs, + fs, io, path::{Path, PathBuf}, }; @@ -10,52 +10,53 @@ use brk_server::{ }; use brk_types::Port; use owo_colors::OwoColorize; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use crate::{default_brk_path, dot_brk_path, fix_user_path}; #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct Config { - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] brkdir: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] brkport: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] website: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] cdn: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] maxweight: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] maxweightlocal: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] cachesize: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] bitcoindir: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] blocksdir: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] rpcconnect: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] rpcport: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] rpccookiefile: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] rpcuser: Option, - #[serde(default, deserialize_with = "default_on_error")] + #[serde(default)] rpcpassword: Option, } @@ -319,10 +320,18 @@ Finally, you can run the program with '-h' for help." } fn read(path: &Path) -> Self { - fs::read_to_string(path).map_or_else( - |_| Config::default(), - |contents| toml::from_str(&contents).unwrap_or_default(), - ) + let contents = match fs::read_to_string(path) { + Ok(contents) => contents, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Config::default(), + Err(e) => { + eprintln!("Cannot read {}: {e}", path.display()); + std::process::exit(1); + } + }; + toml::from_str(&contents).unwrap_or_else(|e| { + eprintln!("Invalid {}:\n{e}", path.display()); + std::process::exit(1); + }) } pub fn rpc(&self) -> Result { @@ -413,14 +422,3 @@ Finally, you can run the program with '-h' for help." self.brkport } } - -fn default_on_error<'de, D, T>(deserializer: D) -> Result -where - D: Deserializer<'de>, - T: Deserialize<'de> + Default, -{ - match T::deserialize(deserializer) { - Ok(v) => Ok(v), - Err(_) => Ok(T::default()), - } -} diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 89b856a27..40adc94ac 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -8897,7 +8897,7 @@ pub struct BrkClient { impl BrkClient { /// Client version. - pub const VERSION: &'static str = "v0.3.0-beta.5"; + pub const VERSION: &'static str = "v0.3.0-beta.6"; /// Create a new client with the given base URL. pub fn new(base_url: impl Into) -> Self { diff --git a/crates/brk_server/src/api/series_legacy.rs b/crates/brk_server/src/api/series_legacy.rs index d13140d51..bb1195e8a 100644 --- a/crates/brk_server/src/api/series_legacy.rs +++ b/crates/brk_server/src/api/series_legacy.rs @@ -70,6 +70,7 @@ struct CostBasisCohortParam { } #[derive(Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] struct CostBasisQuery { #[serde(default)] bucket: UrpdAggregation, diff --git a/crates/brk_server/src/params/addr_txids_param.rs b/crates/brk_server/src/params/addr_txids_param.rs index 05ac1b24d..2f6457f16 100644 --- a/crates/brk_server/src/params/addr_txids_param.rs +++ b/crates/brk_server/src/params/addr_txids_param.rs @@ -4,6 +4,7 @@ use serde::Deserialize; use brk_types::Txid; #[derive(Debug, Default, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct AddrTxidsParam { /// Txid to paginate from (return transactions before this one) pub after_txid: Option, diff --git a/crates/brk_server/src/params/timestamp_param.rs b/crates/brk_server/src/params/timestamp_param.rs index 6df85c9cb..44454dca5 100644 --- a/crates/brk_server/src/params/timestamp_param.rs +++ b/crates/brk_server/src/params/timestamp_param.rs @@ -11,6 +11,7 @@ pub struct TimestampParam { /// Optional UNIX timestamp query parameter #[derive(Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct OptionalTimestampParam { pub timestamp: Option, } diff --git a/crates/brk_server/src/params/urpd_params.rs b/crates/brk_server/src/params/urpd_params.rs index c216ce936..460e8e2bd 100644 --- a/crates/brk_server/src/params/urpd_params.rs +++ b/crates/brk_server/src/params/urpd_params.rs @@ -19,6 +19,7 @@ pub struct UrpdCohortParam { /// Query parameters for URPD endpoints. #[derive(Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct UrpdQuery { /// Aggregation strategy. Default: raw (no aggregation). Accepts `bucket` as alias. #[serde(default, rename = "agg", alias = "bucket")] diff --git a/crates/brk_types/src/pagination.rs b/crates/brk_types/src/pagination.rs index a8cedc144..24a95f3fb 100644 --- a/crates/brk_types/src/pagination.rs +++ b/crates/brk_types/src/pagination.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; /// Pagination parameters for paginated API endpoints #[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct Pagination { /// Pagination index #[serde(default, alias = "p")] diff --git a/crates/brk_types/src/search_query.rs b/crates/brk_types/src/search_query.rs index 7d65056d6..c041c6f0f 100644 --- a/crates/brk_types/src/search_query.rs +++ b/crates/brk_types/src/search_query.rs @@ -4,6 +4,7 @@ use serde::Deserialize; use crate::{Limit, SeriesName}; #[derive(Debug, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct SearchQuery { /// Search query string pub q: SeriesName, diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 8daad2e99..20c786398 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -7271,7 +7271,7 @@ function createTransferPattern(client, acc) { * @extends BrkClientBase */ class BrkClient extends BrkClientBase { - VERSION = "v0.3.0-beta.5"; + VERSION = "v0.3.0-beta.6"; INDEXES = /** @type {const} */ ([ "minute10", diff --git a/modules/brk-client/package.json b/modules/brk-client/package.json index b7c651367..b3e3c3899 100644 --- a/modules/brk-client/package.json +++ b/modules/brk-client/package.json @@ -40,5 +40,5 @@ "url": "git+https://github.com/bitcoinresearchkit/brk.git" }, "type": "module", - "version": "0.3.0-beta.5" + "version": "0.3.0-beta.6" } diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 13dc00b96..5bad2a28c 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -21,10 +21,14 @@ T = TypeVar('T') Addr = str # US Dollar amount Dollars = float +# Amount in satoshis (1 BTC = 100,000,000 sats) +Sats = int # Index within its type (e.g., 0 for first P2WPKH address) TypeIndex = int # Type (P2PKH, P2WPKH, P2SH, P2TR, etc.) OutputType = Literal["p2pk", "p2pk", "p2pkh", "multisig", "p2sh", "op_return", "v0_p2wpkh", "v0_p2wsh", "v1_p2tr", "p2a", "empty", "unknown"] +# Transaction ID (hash) +Txid = str # Unified index for any address type (funded or empty) AnyAddrIndex = TypeIndex # Unsigned basis points stored as u16. @@ -51,10 +55,14 @@ BasisPointsSigned32 = int Bitcoin = float # URL-friendly mining pool identifier PoolSlug = Literal["unknown", "blockfills", "ultimuspool", "terrapool", "luxor", "onethash", "btccom", "bitfarms", "huobipool", "wayicn", "canoepool", "btctop", "bitcoincom", "pool175btc", "gbminers", "axbt", "asicminer", "bitminter", "bitcoinrussia", "btcserv", "simplecoinus", "btcguild", "eligius", "ozcoin", "eclipsemc", "maxbtc", "triplemining", "coinlab", "pool50btc", "ghashio", "stminingcorp", "bitparking", "mmpool", "polmine", "kncminer", "bitalo", "f2pool", "hhtt", "megabigpower", "mtred", "nmcbit", "yourbtcnet", "givemecoins", "braiinspool", "antpool", "multicoinco", "bcpoolio", "cointerra", "kanopool", "solock", "ckpool", "nicehash", "bitclub", "bitcoinaffiliatenetwork", "btcc", "bwpool", "exxbw", "bitsolo", "bitfury", "twentyoneinc", "digitalbtc", "eightbaochi", "mybtccoinpool", "tbdice", "hashpool", "nexious", "bravomining", "hotpool", "okexpool", "bcmonster", "onehash", "bixin", "tatmaspool", "viabtc", "connectbtc", "batpool", "waterhole", "dcexploration", "dcex", "btpool", "fiftyeightcoin", "bitcoinindia", "shawnp0wers", "phashio", "rigpool", "haozhuzhu", "sevenpool", "miningkings", "hashbx", "dpool", "rawpool", "haominer", "helix", "bitcoinukraine", "poolin", "secretsuperstar", "tigerpoolnet", "sigmapoolcom", "okpooltop", "hummerpool", "tangpool", "bytepool", "spiderpool", "novablock", "miningcity", "binancepool", "minerium", "lubiancom", "okkong", "aaopool", "emcdpool", "foundryusa", "sbicrypto", "arkpool", "purebtccom", "marapool", "kucoinpool", "entrustcharitypool", "okminer", "titan", "pegapool", "btcnuggets", "cloudhashing", "digitalxmintsy", "telco214", "btcpoolparty", "multipool", "transactioncoinmining", "btcdig", "trickysbtcpool", "btcmp", "eobot", "unomp", "patels", "gogreenlight", "bitcoinindiapool", "ekanembtc", "canoe", "tiger", "onem1x", "zulupool", "secpool", "ocean", "whitepool", "wiz", "wk057", "futurebitapollosolo", "carbonnegative", "portlandhodl", "phoenix", "neopool", "maxipool", "bitfufupool", "gdpool", "miningdutch", "publicpool", "miningsquared", "innopolistech", "btclab", "parasite", "redrockpool", "est3lar", "braiinssolo", "solopool"] +# Fee rate in sat/vB +FeeRate = float # Weight in weight units (WU). Max block weight is 4,000,000 WU. Weight = int # Block height Height = int +# UNIX timestamp in seconds +Timestamp = int # Block hash BlockHash = str # Transaction index within a block (0 = coinbase) @@ -90,6 +98,8 @@ CostBasisValue = Literal["supply", "realized", "unrealized"] # Options: raw (no aggregation), lin200/lin500/lin1000 (linear $200/$500/$1000), # log10/log50/log100/log200 (logarithmic with 10/50/100/200 buckets per decade). UrpdAggregation = Literal["raw", "lin200", "lin500", "lin1000", "log10", "log50", "log100", "log200"] +# Virtual size in vbytes (weight / 4, rounded up). Max block vsize is ~1,000,000 vB. +VSize = int # Date in YYYYMMDD format stored as u32 Date = int # Output format for API responses @@ -112,6 +122,9 @@ High = Dollars Hour1 = int Hour12 = int Hour4 = int +# Aggregation dimension for querying series. Includes time-based (date, week, month, year), +# block-based (height, tx_index), and address/output type indexes. +Index = Literal["minute10", "minute30", "hour1", "hour4", "hour12", "day1", "day3", "week1", "month1", "month3", "month6", "year1", "year10", "halving", "epoch", "height", "tx_index", "txin_index", "txout_index", "empty_output_index", "op_return_index", "p2a_addr_index", "p2ms_output_index", "p2pk33_addr_index", "p2pk65_addr_index", "p2pkh_addr_index", "p2sh_addr_index", "p2tr_addr_index", "p2wpkh_addr_index", "p2wsh_addr_index", "unknown_output_index", "funded_addr_index", "empty_addr_index"] # Series name SeriesName = str # Lowest price value for a time period @@ -201,6 +214,8 @@ Witness = List[str] # Unlike TxVersion (u8, indexed), this preserves non-standard values # used in coinbase txs for miner signaling/branding. TxVersionRaw = int +# Hierarchical tree node for organizing series into categories +TreeNode = Union[dict[str, "TreeNode"], "SeriesLeafWithSchema"] TxInIndex = int TxOutIndex = int # Input index in the spending transaction @@ -211,21 +226,6 @@ UnknownOutputIndex = TypeIndex Week1 = int Year1 = int Year10 = int -# Fee rate in sat/vB -FeeRate = float -# Aggregation dimension for querying series. Includes time-based (date, week, month, year), -# block-based (height, tx_index), and address/output type indexes. -Index = Literal["minute10", "minute30", "hour1", "hour4", "hour12", "day1", "day3", "week1", "month1", "month3", "month6", "year1", "year10", "halving", "epoch", "height", "tx_index", "txin_index", "txout_index", "empty_output_index", "op_return_index", "p2a_addr_index", "p2ms_output_index", "p2pk33_addr_index", "p2pk65_addr_index", "p2pkh_addr_index", "p2sh_addr_index", "p2tr_addr_index", "p2wpkh_addr_index", "p2wsh_addr_index", "unknown_output_index", "funded_addr_index", "empty_addr_index"] -# Amount in satoshis (1 BTC = 100,000,000 sats) -Sats = int -# UNIX timestamp in seconds -Timestamp = int -# Hierarchical tree node for organizing series into categories -TreeNode = Union[dict[str, "TreeNode"], "SeriesLeafWithSchema"] -# Transaction ID (hash) -Txid = str -# Virtual size in vbytes (weight / 4, rounded up). Max block vsize is ~1,000,000 vB. -VSize = int class AddrChainStats(TypedDict): """ Address statistics on the blockchain (confirmed transactions only) @@ -1241,6 +1241,45 @@ class Prices(TypedDict): time: Timestamp USD: Dollars +class RbfTx(TypedDict): + """ + Transaction summary carried inside an RBF replacement node. Shape + matches mempool.space's `/api/v1/tx/:txid/rbf` and + `/api/v1/replacements` responses. + + Attributes: + value: Sum of output amounts. + rbf: BIP-125 signaling: at least one input has sequence < 0xffffffff-1. + fullRbf: Only populated on the root `tx` of an RBF response. `true` iff +this tx displaced at least one non-signaling predecessor. + """ + txid: Txid + fee: Sats + vsize: VSize + value: Sats + rate: FeeRate + time: Timestamp + rbf: bool + fullRbf: Optional[bool] + +class ReplacementNode(TypedDict): + """ + One node in an RBF replacement tree. The node's `tx` replaced each + entry in `replaces`, recursively. + + Attributes: + time: First-seen timestamp, duplicated here to match mempool.space's +on-the-wire shape. + fullRbf: Any predecessor in this subtree was non-signaling. + interval: Seconds between this node's `time` and the successor that +replaced it. Omitted on the root of an RBF response. + """ + tx: RbfTx + time: Timestamp + fullRbf: bool + interval: Optional[int] + replaces: List["ReplacementNode"] + class RbfResponse(TypedDict): """ Response body for `GET /api/v1/tx/:txid/rbf`. Both fields are null @@ -1304,6 +1343,21 @@ class SeriesInfo(TypedDict): indexes: List[Index] type: str +class SeriesLeafWithSchema(TypedDict): + """ + SeriesLeaf with JSON Schema for client generation + + Attributes: + name: The series name/identifier + kind: The Rust type (e.g., "Sats", "StoredF64") + indexes: Available indexes for this series + type: JSON Schema type (e.g., "integer", "number", "string", "boolean", "array", "object") + """ + name: str + kind: str + indexes: List[Index] + type: str + class SeriesNameWithIndex(TypedDict): """ Attributes: @@ -1595,60 +1649,6 @@ class ValidateAddrParam(TypedDict): """ address: str -class RbfTx(TypedDict): - """ - Transaction summary carried inside an RBF replacement node. Shape - matches mempool.space's `/api/v1/tx/:txid/rbf` and - `/api/v1/replacements` responses. - - Attributes: - value: Sum of output amounts. - rbf: BIP-125 signaling: at least one input has sequence < 0xffffffff-1. - fullRbf: Only populated on the root `tx` of an RBF response. `true` iff -this tx displaced at least one non-signaling predecessor. - """ - txid: Txid - fee: Sats - vsize: VSize - value: Sats - rate: FeeRate - time: Timestamp - rbf: bool - fullRbf: Optional[bool] - -class ReplacementNode(TypedDict): - """ - One node in an RBF replacement tree. The node's `tx` replaced each - entry in `replaces`, recursively. - - Attributes: - time: First-seen timestamp, duplicated here to match mempool.space's -on-the-wire shape. - fullRbf: Any predecessor in this subtree was non-signaling. - interval: Seconds between this node's `time` and the successor that -replaced it. Omitted on the root of an RBF response. - """ - tx: RbfTx - time: Timestamp - fullRbf: bool - interval: Optional[int] - replaces: List["ReplacementNode"] - -class SeriesLeafWithSchema(TypedDict): - """ - SeriesLeaf with JSON Schema for client generation - - Attributes: - name: The series name/identifier - kind: The Rust type (e.g., "Sats", "StoredF64") - indexes: Available indexes for this series - type: JSON Schema type (e.g., "integer", "number", "string", "boolean", "array", "object") - """ - name: str - kind: str - indexes: List[Index] - type: str - class BrkError(Exception): """Custom error class for BRK client errors.""" @@ -6522,7 +6522,7 @@ class SeriesTree: class BrkClient(BrkClientBase): """Main BRK client with series tree and API methods.""" - VERSION = "v0.3.0-beta.5" + VERSION = "v0.3.0-beta.6" INDEXES = [ "minute10", diff --git a/packages/brk_client/pyproject.toml b/packages/brk_client/pyproject.toml index 2bcdf01c8..a7566d209 100644 --- a/packages/brk_client/pyproject.toml +++ b/packages/brk_client/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "brk-client" -version = "0.3.0-beta.5" +version = "0.3.0-beta.6" description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index" readme = "README.md" requires-python = ">=3.9" diff --git a/packages/brk_client/uv.lock b/packages/brk_client/uv.lock index 4e7ae5f3f..19e5e4a29 100644 --- a/packages/brk_client/uv.lock +++ b/packages/brk_client/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "brk-client" -version = "0.3.0b2" +version = "0.3.0b6" source = { editable = "." } [package.dev-dependencies] diff --git a/scripts/cf-purge.sh b/scripts/cf-purge.sh index e758a22f0..f97ab8fba 100755 --- a/scripts/cf-purge.sh +++ b/scripts/cf-purge.sh @@ -10,8 +10,8 @@ fi echo "=== Cloudflare cache purge ===" echo "" -if [ -z "$CF_API_TOKEN" ]; then - echo "CF_API_TOKEN not set. Add it to scripts/.tokens" +if [ -z "$CF_PURGE_API_TOKEN" ]; then + echo "CF_PURGE_API_TOKEN not set. Add it to scripts/.tokens" exit 1 fi if [ -z "$CF_ZONE_ID" ]; then @@ -21,7 +21,7 @@ fi RESPONSE=$(curl -sS -X POST \ "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \ - -H "Authorization: Bearer $CF_API_TOKEN" \ + -H "Authorization: Bearer $CF_PURGE_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"purge_everything":true}')