diff --git a/crates/brk_bindgen/src/generators/rust/api.rs b/crates/brk_bindgen/src/generators/rust/api.rs index 05bad3255..fe9469166 100644 --- a/crates/brk_bindgen/src/generators/rust/api.rs +++ b/crates/brk_bindgen/src/generators/rust/api.rs @@ -144,7 +144,15 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { writeln!(output, " let mut query = Vec::new();").unwrap(); for param in &endpoint.query_params { let ident = sanitize_ident(¶m.name); - if param.required { + let is_array = param.param_type.ends_with("[]"); + if is_array { + writeln!( + output, + " for v in {} {{ query.push(format!(\"{}={{}}\", v)); }}", + ident, param.name + ) + .unwrap(); + } else if param.required { writeln!( output, " query.push(format!(\"{}={{}}\", {}));", diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 8ba65651f..ade82ce72 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -8734,6 +8734,17 @@ impl BrkClient { self.base.get_json(&format!("/api/tx/{txid}/merkle-proof")) } + /// Transaction merkleblock proof + /// + /// Get the merkleblock proof for a transaction (BIP37 format, hex encoded). + /// + /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-merkleblock-proof)* + /// + /// Endpoint: `GET /api/tx/{txid}/merkleblock-proof` + pub fn get_tx_merkleblock_proof(&self, txid: Txid) -> Result { + self.base.get_json(&format!("/api/tx/{txid}/merkleblock-proof")) + } + /// Output spend status /// /// Get the spending status of a transaction output. Returns whether the output has been spent and, if so, the spending transaction details. @@ -9079,6 +9090,17 @@ impl BrkClient { self.base.get_json(&format!("/api/v1/mining/reward-stats/{block_count}")) } + /// Current BTC price + /// + /// Returns bitcoin latest price (on-chain derived, USD only). + /// + /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)* + /// + /// Endpoint: `GET /api/v1/prices` + pub fn get_prices(&self) -> Result { + self.base.get_json(&format!("/api/v1/prices")) + } + /// Transaction first-seen times /// /// Returns timestamps when transactions were first seen in the mempool. Returns 0 for mined or unknown transactions. @@ -9086,12 +9108,8 @@ impl BrkClient { /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-times)* /// /// Endpoint: `GET /api/v1/transaction-times` - pub fn get_transaction_times(&self, txId: &[Txid]) -> Result> { - let mut query = Vec::new(); - query.push(format!("txId[]={}", txId)); - let query_str = if query.is_empty() { String::new() } else { format!("?{}", query.join("&")) }; - let path = format!("/api/v1/transaction-times{}", query_str); - self.base.get_json(&path) + pub fn get_transaction_times(&self) -> Result> { + self.base.get_json(&format!("/api/v1/transaction-times")) } /// Validate address diff --git a/crates/brk_indexer/src/processor/metadata.rs b/crates/brk_indexer/src/processor/metadata.rs index 318bb5115..a96f6f91d 100644 --- a/crates/brk_indexer/src/processor/metadata.rs +++ b/crates/brk_indexer/src/processor/metadata.rs @@ -58,10 +58,10 @@ impl BlockProcessor<'_> { let mut sw_size = 0usize; let mut sw_weight = 0usize; - for tx in txs { + for (i, tx) in txs.iter().enumerate() { total_size += tx.total_size as usize; weight += tx.weight(); - if tx.is_segwit() { + if i > 0 && tx.is_segwit() { sw_txs += 1; sw_size += tx.total_size as usize; sw_weight += tx.weight(); diff --git a/crates/brk_server/src/api/mempool/mod.rs b/crates/brk_server/src/api/mempool/mod.rs index b13e274e7..48b7d56cc 100644 --- a/crates/brk_server/src/api/mempool/mod.rs +++ b/crates/brk_server/src/api/mempool/mod.rs @@ -5,7 +5,7 @@ use axum::{ }; use brk_types::{ Dollars, HistoricalPrice, MempoolBlock, MempoolInfo, MempoolRecentTx, OptionalTimestampParam, - RecommendedFees, Txid, + Prices, RecommendedFees, Timestamp, Txid, }; use crate::{CacheStrategy, extended::TransformResponseExtended}; @@ -67,6 +67,27 @@ impl MempoolRoutes for ApiRouter { }, ), ) + .api_route( + "/api/v1/prices", + get_with( + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, state.mempool_cache(), &uri, |q| { + Ok(Prices { + time: Timestamp::now(), + usd: q.live_price()?, + }) + }).await + }, + |op| { + op.id("get_prices") + .mempool_tag() + .summary("Current BTC price") + .description("Returns bitcoin latest price (on-chain derived, USD only).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)*") + .ok_response::() + .server_error() + }, + ), + ) .api_route( "/api/mempool/price", get_with( diff --git a/crates/brk_server/src/api/transactions/mod.rs b/crates/brk_server/src/api/transactions/mod.rs index 3cd99c700..42afaa7c6 100644 --- a/crates/brk_server/src/api/transactions/mod.rs +++ b/crates/brk_server/src/api/transactions/mod.rs @@ -6,7 +6,6 @@ use axum::{ extract::{Path, State}, http::{HeaderMap, Uri}, }; -use axum::extract::Query; use brk_types::{ CpfpInfo, Hex, MerkleProof, Transaction, TxOutspend, TxStatus, Txid, TxidParam, TxidVout, TxidsParam, @@ -169,6 +168,24 @@ impl TxRoutes for ApiRouter { }, ), ) + .api_route( + "/api/tx/{txid}/merkleblock-proof", + get_with( + async |uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State| { + state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.merkleblock_proof(txid)).await + }, + |op| op + .id("get_tx_merkleblock_proof") + .transactions_tag() + .summary("Transaction merkleblock proof") + .description("Get the merkleblock proof for a transaction (BIP37 format, hex encoded).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-merkleblock-proof)*") + .ok_response::() + .not_modified() + .bad_request() + .not_found() + .server_error(), + ), + ) .api_route( "/api/tx/{txid}/raw", get_with( @@ -224,7 +241,8 @@ impl TxRoutes for ApiRouter { .api_route( "/api/v1/transaction-times", get_with( - async |uri: Uri, headers: HeaderMap, Query(params): Query, State(state): State| { + async |uri: Uri, headers: HeaderMap, State(state): State| { + let params = TxidsParam::from_query(uri.query().unwrap_or("")); state.cached_json(&headers, CacheStrategy::MempoolHash(0), &uri, move |q| q.transaction_times(¶ms.txids)).await }, |op| op diff --git a/crates/brk_types/src/historical_price.rs b/crates/brk_types/src/historical_price.rs index 74725a4f4..3cc1a993d 100644 --- a/crates/brk_types/src/historical_price.rs +++ b/crates/brk_types/src/historical_price.rs @@ -1,7 +1,15 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::Dollars; +use crate::{Dollars, Timestamp}; + +/// Current price response matching mempool.space /api/v1/prices format +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct Prices { + pub time: Timestamp, + #[serde(rename = "USD")] + pub usd: Dollars, +} /// Historical price response #[derive(Debug, Serialize, Deserialize, JsonSchema)] diff --git a/crates/brk_types/src/txid.rs b/crates/brk_types/src/txid.rs index 82e1562af..bc40ff69a 100644 --- a/crates/brk_types/src/txid.rs +++ b/crates/brk_types/src/txid.rs @@ -63,6 +63,14 @@ impl fmt::Display for Txid { } } +impl FromStr for Txid { + type Err = bitcoin::hashes::hex::HexToArrayError; + + fn from_str(s: &str) -> Result { + bitcoin::Txid::from_str(s).map(Self::from) + } +} + impl Serialize for Txid { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/brk_types/src/txids_param.rs b/crates/brk_types/src/txids_param.rs index 33b88afc4..65fb25dbb 100644 --- a/crates/brk_types/src/txids_param.rs +++ b/crates/brk_types/src/txids_param.rs @@ -1,10 +1,31 @@ +use std::str::FromStr; + use schemars::JsonSchema; -use serde::Deserialize; use crate::Txid; -#[derive(Deserialize, JsonSchema)] +/// Query parameter for transaction-times endpoint. +#[derive(JsonSchema)] pub struct TxidsParam { #[serde(rename = "txId[]")] pub txids: Vec, } + +impl TxidsParam { + /// Parsed manually from URI since serde_urlencoded doesn't support repeated keys. + pub fn from_query(query: &str) -> Self { + Self { + txids: query + .split('&') + .filter_map(|pair| { + let (key, val) = pair.split_once('=')?; + if key == "txId[]" || key == "txId%5B%5D" { + Txid::from_str(val).ok() + } else { + None + } + }) + .collect(), + } + } +} diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 725318207..3393c004d 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -789,6 +789,13 @@ * @property {number} blockCount - Total blocks in the time period * @property {number} lastEstimatedHashrate - Estimated network hashrate (hashes per second) */ +/** + * Current price response matching mempool.space /api/v1/prices format + * + * @typedef {Object} Prices + * @property {Timestamp} time + * @property {Dollars} uSD + */ /** * A range boundary: integer index, date, or timestamp. * @@ -1063,10 +1070,6 @@ * @property {Txid} txid - Transaction ID * @property {Vout} vout - Output index */ -/** - * @typedef {Object} TxidsParam - * @property {Txid[]} txId[] - */ /** * Index within its type (e.g., 0 for first P2WPKH address) * @@ -10098,6 +10101,22 @@ class BrkClient extends BrkClientBase { return this.getJson(`/api/tx/${txid}/merkle-proof`); } + /** + * Transaction merkleblock proof + * + * Get the merkleblock proof for a transaction (BIP37 format, hex encoded). + * + * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-merkleblock-proof)* + * + * Endpoint: `GET /api/tx/{txid}/merkleblock-proof` + * + * @param {Txid} txid + * @returns {Promise} + */ + async getTxMerkleblockProof(txid) { + return this.getJson(`/api/tx/${txid}/merkleblock-proof`); + } + /** * Output spend status * @@ -10582,6 +10601,20 @@ class BrkClient extends BrkClientBase { return this.getJson(`/api/v1/mining/reward-stats/${block_count}`); } + /** + * Current BTC price + * + * Returns bitcoin latest price (on-chain derived, USD only). + * + * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)* + * + * Endpoint: `GET /api/v1/prices` + * @returns {Promise} + */ + async getPrices() { + return this.getJson(`/api/v1/prices`); + } + /** * Transaction first-seen times * @@ -10590,16 +10623,10 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-times)* * * Endpoint: `GET /api/v1/transaction-times` - * - * @param {Txid[]} [txId[]] * @returns {Promise} */ - async getTransactionTimes(txId) { - const params = new URLSearchParams(); - params.set('txId[]', String(txId)); - const query = params.toString(); - const path = `/api/v1/transaction-times${query ? '?' + query : ''}`; - return this.getJson(path); + async getTransactionTimes() { + return this.getJson(`/api/v1/transaction-times`); } /** diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index fcb1b3114..41196c191 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -1078,6 +1078,13 @@ class PoolsSummary(TypedDict): blockCount: int lastEstimatedHashrate: int +class Prices(TypedDict): + """ + Current price response matching mempool.space /api/v1/prices format + """ + time: Timestamp + USD: Dollars + class RecommendedFees(TypedDict): """ Recommended fee rates in sat/vB @@ -1314,9 +1321,6 @@ class TxidVout(TypedDict): txid: Txid vout: Vout -class TxidsParam(TypedDict): - txId: List[Txid] - class Utxo(TypedDict): """ Unspent transaction output @@ -7495,6 +7499,16 @@ class BrkClient(BrkClientBase): Endpoint: `GET /api/tx/{txid}/merkle-proof`""" return self.get_json(f'/api/tx/{txid}/merkle-proof') + def get_tx_merkleblock_proof(self, txid: Txid) -> str: + """Transaction merkleblock proof. + + Get the merkleblock proof for a transaction (BIP37 format, hex encoded). + + *[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-merkleblock-proof)* + + Endpoint: `GET /api/tx/{txid}/merkleblock-proof`""" + return self.get_json(f'/api/tx/{txid}/merkleblock-proof') + def get_tx_outspend(self, txid: Txid, vout: Vout) -> TxOutspend: """Output spend status. @@ -7809,7 +7823,17 @@ class BrkClient(BrkClientBase): Endpoint: `GET /api/v1/mining/reward-stats/{block_count}`""" return self.get_json(f'/api/v1/mining/reward-stats/{block_count}') - def get_transaction_times(self, txId: List[Txid]) -> List[float]: + def get_prices(self) -> Prices: + """Current BTC price. + + Returns bitcoin latest price (on-chain derived, USD only). + + *[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)* + + Endpoint: `GET /api/v1/prices`""" + return self.get_json('/api/v1/prices') + + def get_transaction_times(self) -> List[float]: """Transaction first-seen times. Returns timestamps when transactions were first seen in the mempool. Returns 0 for mined or unknown transactions. @@ -7817,11 +7841,7 @@ class BrkClient(BrkClientBase): *[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-times)* Endpoint: `GET /api/v1/transaction-times`""" - params = [] - params.append(f'txId[]={txId}') - query = '&'.join(params) - path = f'/api/v1/transaction-times{"?" + query if query else ""}' - return self.get_json(path) + return self.get_json('/api/v1/transaction-times') def validate_address(self, address: str) -> AddrValidation: """Validate address.