From 9cb5f2c880591ebc684b4530fc7c0161bdf089f0 Mon Sep 17 00:00:00 2001 From: nym21 Date: Sun, 3 May 2026 00:57:22 +0200 Subject: [PATCH] global: fixes --- crates/brk_client/src/lib.rs | 2 +- crates/brk_query/src/impl/addr.rs | 7 +- crates/brk_query/src/impl/block/raw.rs | 4 +- crates/brk_query/src/impl/mempool.rs | 31 +++++---- .../brk_query/src/impl/mining/difficulty.rs | 13 +++- crates/brk_server/src/api/addrs.rs | 4 +- crates/brk_types/src/timestamp.rs | 7 ++ modules/brk-client/index.js | 2 +- packages/brk_client/brk_client/__init__.py | 2 +- .../addresses/test_address_txs.py | 69 +++++++------------ .../general/test_difficulty_adjustment.py | 4 +- .../mempool_compat/transactions/test_cpfp.py | 23 +++++++ .../transactions/test_tx_status.py | 26 +++++++ 13 files changed, 120 insertions(+), 74 deletions(-) diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 27e2fc146..c1ca8224a 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -9026,7 +9026,7 @@ impl BrkClient { /// Address transactions /// - /// Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`. + /// Get transaction history for an address, sorted with newest first. Returns up to 50 entries: mempool transactions first, then confirmed transactions filling the remainder. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* /// diff --git a/crates/brk_query/src/impl/addr.rs b/crates/brk_query/src/impl/addr.rs index f88bcf478..fe2742baf 100644 --- a/crates/brk_query/src/impl/addr.rs +++ b/crates/brk_query/src/impl/addr.rs @@ -90,19 +90,20 @@ impl Query { } /// Esplora `/address/:address/txs` first page: up to `mempool_limit` - /// mempool (newest first) followed by the first `chain_limit` - /// confirmed. Pagination is path-style via `/txs/chain/:after_txid`. + /// mempool entries (newest first), then chain entries fill the response + /// up to `total_limit`. Pagination is path-style via `/txs/chain/:after_txid`. pub fn addr_txs( &self, addr: Addr, + total_limit: usize, mempool_limit: usize, - chain_limit: usize, ) -> Result> { let mut out = if self.mempool().is_some() { self.addr_mempool_txs(&addr, mempool_limit)? } else { Vec::new() }; + let chain_limit = total_limit.saturating_sub(out.len()); out.extend(self.addr_txs_chain(&addr, None, chain_limit)?); Ok(out) } diff --git a/crates/brk_query/src/impl/block/raw.rs b/crates/brk_query/src/impl/block/raw.rs index 5e82499c9..2273c3466 100644 --- a/crates/brk_query/src/impl/block/raw.rs +++ b/crates/brk_query/src/impl/block/raw.rs @@ -16,7 +16,9 @@ impl Query { let max_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1)); if height > max_height { - return Err(Error::OutOfRange("Block height out of range".into())); + return Err(Error::OutOfRange(format!( + "Block height {height} out of range (tip {max_height})" + ))); } let position = indexer.vecs.blocks.position.collect_one(height).data()?; diff --git a/crates/brk_query/src/impl/mempool.rs b/crates/brk_query/src/impl/mempool.rs index cbbebc9df..ec71711a3 100644 --- a/crates/brk_query/src/impl/mempool.rs +++ b/crates/brk_query/src/impl/mempool.rs @@ -13,25 +13,25 @@ use crate::Query; const RECENT_REPLACEMENTS_LIMIT: usize = 25; impl Query { + fn require_mempool(&self) -> Result<&Mempool> { + self.mempool().ok_or(Error::MempoolNotAvailable) + } + pub fn mempool_info(&self) -> Result { - let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?; - Ok(mempool.info()) + Ok(self.require_mempool()?.info()) } pub fn mempool_txids(&self) -> Result> { - let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?; - let txs = mempool.txs(); + let txs = self.require_mempool()?.txs(); Ok(txs.keys().cloned().collect()) } pub fn recommended_fees(&self) -> Result { - self.mempool() - .map(|mempool| mempool.fees()) - .ok_or(Error::MempoolNotAvailable) + self.require_mempool().map(|m| m.fees()) } pub fn mempool_blocks(&self) -> Result> { - let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?; + let mempool = self.require_mempool()?; let block_stats = mempool.block_stats(); @@ -90,8 +90,7 @@ impl Query { } pub fn mempool_recent(&self) -> Result> { - let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?; - Ok(mempool.txs().recent().to_vec()) + Ok(self.require_mempool()?.txs().recent().to_vec()) } /// CPFP cluster for `txid`. Returns the mempool cluster when the txid is @@ -289,7 +288,7 @@ impl Query { /// walks `predecessors_of` backward to build the tree. `replaces` /// is the requested tx's own direct predecessors. pub fn tx_rbf(&self, txid: &Txid) -> Result { - let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?; + let mempool = self.require_mempool()?; let txs = mempool.txs(); let entries = mempool.entries(); let graveyard = mempool.graveyard(); @@ -422,7 +421,7 @@ impl Query { /// true, only trees with at least one non-signaling predecessor /// are returned. pub fn recent_replacements(&self, full_rbf_only: bool) -> Result> { - let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?; + let mempool = self.require_mempool()?; let txs = mempool.txs(); let entries = mempool.entries(); let graveyard = mempool.graveyard(); @@ -450,15 +449,17 @@ impl Query { .collect()) } + /// `first_seen` Unix-second timestamps for each txid, matching + /// mempool.space's `POST /api/v1/transaction-times`. Returns 0 for + /// unknown txids, in input order. pub fn transaction_times(&self, txids: &[Txid]) -> Result> { - let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?; - let entries = mempool.entries(); + let entries = self.require_mempool()?.entries(); Ok(txids .iter() .map(|txid| { entries .get(&TxidPrefix::from(txid)) - .map(|e| usize::from(e.first_seen) as u64) + .map(|e| u64::from(e.first_seen)) .unwrap_or(0) }) .collect()) diff --git a/crates/brk_query/src/impl/mining/difficulty.rs b/crates/brk_query/src/impl/mining/difficulty.rs index e9939038f..30d81a6fd 100644 --- a/crates/brk_query/src/impl/mining/difficulty.rs +++ b/crates/brk_query/src/impl/mining/difficulty.rs @@ -65,8 +65,17 @@ impl Query { TARGET_BLOCK_TIME }; + // Per-block time needed over remaining blocks to land the epoch at + // 2016 * TARGET_BLOCK_TIME. Matches mempool.space's adjustedTimeAvg. + let target_total = BLOCKS_PER_EPOCH as u64 * TARGET_BLOCK_TIME; + let adjusted_time_avg = if remaining_blocks > 0 { + target_total.saturating_sub(elapsed_time) / remaining_blocks as u64 + } else { + TARGET_BLOCK_TIME + }; + // Estimate remaining time and retarget date - let remaining_time = remaining_blocks as u64 * time_avg; + let remaining_time = remaining_blocks as u64 * adjusted_time_avg; let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) @@ -131,7 +140,7 @@ impl Query { previous_time, next_retarget_height: Height::from(next_retarget_height), time_avg: time_avg * 1000, - adjusted_time_avg: time_avg * 1000, + adjusted_time_avg: adjusted_time_avg * 1000, time_offset, expected_blocks, }) diff --git a/crates/brk_server/src/api/addrs.rs b/crates/brk_server/src/api/addrs.rs index dd3e3da3a..72fbad3d7 100644 --- a/crates/brk_server/src/api/addrs.rs +++ b/crates/brk_server/src/api/addrs.rs @@ -50,12 +50,12 @@ impl AddrRoutes for ApiRouter { State(state): State | { let strategy = state.addr_strategy(Version::ONE, &path.addr, false); - state.respond_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, 50, 25)).await + state.respond_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, 50, 50)).await }, |op| op .id("get_address_txs") .addrs_tag() .summary("Address transactions") - .description("Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*") + .description("Get transaction history for an address, sorted with newest first. Returns up to 50 entries: mempool transactions first, then confirmed transactions filling the remainder. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*") .json_response::>() .not_modified() .bad_request() diff --git a/crates/brk_types/src/timestamp.rs b/crates/brk_types/src/timestamp.rs index e6653c743..906dc6c6b 100644 --- a/crates/brk_types/src/timestamp.rs +++ b/crates/brk_types/src/timestamp.rs @@ -137,6 +137,13 @@ impl From for usize { } } +impl From for u64 { + #[inline] + fn from(value: Timestamp) -> Self { + u64::from(value.0) + } +} + impl From for Timestamp { #[inline] fn from(value: Date) -> Self { diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 7ef309751..bf4d76dbf 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -10442,7 +10442,7 @@ class BrkClient extends BrkClientBase { /** * Address transactions * - * Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`. + * Get transaction history for an address, sorted with newest first. Returns up to 50 entries: mempool transactions first, then confirmed transactions filling the remainder. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* * diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 5f3640521..2c34a578a 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -7793,7 +7793,7 @@ class BrkClient(BrkClientBase): def get_address_txs(self, address: Addr) -> List[Transaction]: """Address transactions. - Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`. + Get transaction history for an address, sorted with newest first. Returns up to 50 entries: mempool transactions first, then confirmed transactions filling the remainder. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* diff --git a/packages/brk_client/tests/mempool_compat/addresses/test_address_txs.py b/packages/brk_client/tests/mempool_compat/addresses/test_address_txs.py index e3a4f5451..3c646544b 100644 --- a/packages/brk_client/tests/mempool_compat/addresses/test_address_txs.py +++ b/packages/brk_client/tests/mempool_compat/addresses/test_address_txs.py @@ -49,67 +49,44 @@ def test_address_txs_shape_dynamic(brk, mempool, live_addrs): @pytest.mark.parametrize("addr", STATIC_ADDRS) def test_address_txs_ordering(brk, addr): - """All entries must be confirmed and heights monotonically non-increasing.""" + """Response is mempool-prefix (unconfirmed, newest-first) + chain-suffix (confirmed, height-desc).""" b = brk.get_address_txs(addr) if not b: pytest.skip(f"{addr} has no txs in brk") - for tx in b: - assert tx["status"]["confirmed"] is True, ( - f"{addr} returned unconfirmed tx {tx['txid']} (this endpoint is chain-only on brk)" - ) - heights = [tx["status"]["block_height"] for tx in b] + + confirmed_flags = [tx["status"]["confirmed"] for tx in b] + assert confirmed_flags == sorted(confirmed_flags), ( + f"{addr}: confirmed flags must be False*..*True* (mempool prefix then chain), got " + f"{confirmed_flags[:10]}..." + ) + + chain = [tx for tx in b if tx["status"]["confirmed"]] + heights = [tx["status"]["block_height"] for tx in chain] assert heights == sorted(heights, reverse=True), ( - f"{addr} not newest-first by height: {heights[:5]}..." + f"{addr} chain segment not newest-first by height: {heights[:5]}..." ) @pytest.mark.parametrize("addr", STATIC_ADDRS) def test_address_txs_limit(brk, addr): - """Hard cap of 50 confirmed txs per call.""" + """Hard cap of 50 entries per call (mempool first, chain fills remainder).""" b = brk.get_address_txs(addr) assert len(b) <= 50, f"{addr} returned {len(b)} txs, exceeds 50-cap" @pytest.mark.parametrize("addr", STABLE_ADDRS) def test_address_txs_top_match_stable(brk, mempool, addr): - """For inactive historical addresses, brk and mempool agree on first-page order.""" - b_txids = [t["txid"] for t in brk.get_address_txs(addr)] - m_txids = [t["txid"] for t in mempool.get_json(f"/api/address/{addr}/txs")] - assert b_txids == m_txids, ( - f"{addr} first-page txid order diverges:\n" - f" brk: {b_txids[:5]}...\n" - f" mempool: {m_txids[:5]}..." - ) - - -def test_address_txs_pagination(brk, mempool): - """`after_txid` returns a fresh, strictly-older page; matches mempool.space.""" - addr = "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r" - first = brk.get_address_txs(addr) - assert len(first) == 50, f"expected full first page, got {len(first)}" - last_txid = first[-1]["txid"] - last_height = first[-1]["status"]["block_height"] - - second = brk.get_address_txs(addr, after_txid=last_txid) - assert second, "second page must be non-empty for a 5700-tx address" - - first_txids = {t["txid"] for t in first} - second_txids = {t["txid"] for t in second} - assert not (first_txids & second_txids), "pagination must not return overlapping txs" - - for tx in second: - assert tx["status"]["block_height"] <= last_height, ( - f"page 2 tx {tx['txid']} at height {tx['status']['block_height']} " - f"exceeds page-1 tail height {last_height}" - ) - - m_second = mempool.get_json(f"/api/address/{addr}/txs?after_txid={last_txid}") - b_ids = [t["txid"] for t in second] - m_ids = [t["txid"] for t in m_second] - assert b_ids == m_ids, ( - f"page-2 order diverges from mempool:\n" - f" brk: {b_ids[:5]}...\n" - f" mempool: {m_ids[:5]}..." + """For inactive historical addresses, the confirmed tail must agree exactly with mempool.space.""" + b_chain = [t["txid"] for t in brk.get_address_txs(addr) if t["status"]["confirmed"]] + m_chain = [ + t["txid"] + for t in mempool.get_json(f"/api/address/{addr}/txs") + if t["status"]["confirmed"] + ] + assert b_chain == m_chain, ( + f"{addr} confirmed-tail txid order diverges:\n" + f" brk: {b_chain[:5]}...\n" + f" mempool: {m_chain[:5]}..." ) diff --git a/packages/brk_client/tests/mempool_compat/general/test_difficulty_adjustment.py b/packages/brk_client/tests/mempool_compat/general/test_difficulty_adjustment.py index ee7e1fca8..30ebd2a92 100644 --- a/packages/brk_client/tests/mempool_compat/general/test_difficulty_adjustment.py +++ b/packages/brk_client/tests/mempool_compat/general/test_difficulty_adjustment.py @@ -69,8 +69,8 @@ def test_difficulty_adjustment_invariants(brk): assert 1_000 <= d["timeAvg"] <= 3_600_000 assert 1_000 <= d["adjustedTimeAvg"] <= 3_600_000 - # remainingTime is constructed as remainingBlocks * timeAvg in brk. - assert d["remainingTime"] == d["remainingBlocks"] * d["timeAvg"] + # remainingTime is remainingBlocks * adjustedTimeAvg (matches mempool.space). + assert d["remainingTime"] == d["remainingBlocks"] * d["adjustedTimeAvg"] assert d["estimatedRetargetDate"] > now_ms assert d["previousTime"] * 1000 < now_ms diff --git a/packages/brk_client/tests/mempool_compat/transactions/test_cpfp.py b/packages/brk_client/tests/mempool_compat/transactions/test_cpfp.py index bf64a758a..42f2267e5 100644 --- a/packages/brk_client/tests/mempool_compat/transactions/test_cpfp.py +++ b/packages/brk_client/tests/mempool_compat/transactions/test_cpfp.py @@ -55,3 +55,26 @@ def test_cpfp_malformed_short(brk, bad): with pytest.raises(BrkError) as exc_info: brk.get_text(f"/api/v1/cpfp/{bad}") assert exc_info.value.status == 400 + + +def test_cpfp_mempool_unconfirmed(brk, mempool): + """Unconfirmed mempool tx: brk and mempool.space agree on cpfp shape.""" + txids = mempool.get_json("/api/mempool/txids") + if not txids: + pytest.skip("mempool.space mempool currently empty") + + for txid in txids[:50]: + try: + b = brk.get_cpfp(txid) + except BrkError: + continue + try: + m = mempool.get_json(f"/api/v1/cpfp/{txid}") + except Exception: + continue + show("GET", f"/api/v1/cpfp/{txid}", b, m) + assert_same_structure(b, m) + assert isinstance(b.get("ancestors"), list) + assert isinstance(b.get("descendants", []), list) + return + pytest.skip("no shared unconfirmed tx between brk and mempool.space") diff --git a/packages/brk_client/tests/mempool_compat/transactions/test_tx_status.py b/packages/brk_client/tests/mempool_compat/transactions/test_tx_status.py index fa78929df..70f0a41db 100644 --- a/packages/brk_client/tests/mempool_compat/transactions/test_tx_status.py +++ b/packages/brk_client/tests/mempool_compat/transactions/test_tx_status.py @@ -49,3 +49,29 @@ def test_tx_status_malformed_unknown(brk): with pytest.raises(BrkError) as exc_info: brk.get_text(f"/api/tx/{bad}/status") assert exc_info.value.status == 404 + + +def test_tx_status_mempool_unconfirmed(brk, mempool): + """Unconfirmed mempool tx: status must be confirmed=false with no block fields.""" + txids = mempool.get_json("/api/mempool/txids") + if not txids: + pytest.skip("mempool.space mempool currently empty") + + for txid in txids[:25]: + try: + b = brk.get_tx_status(txid) + except BrkError: + continue + if b.get("confirmed"): + continue + try: + m = mempool.get_json(f"/api/tx/{txid}/status") + except Exception: + continue + if m.get("confirmed"): + continue + show("GET", f"/api/tx/{txid}/status", b, m) + assert_same_values(b, m) + assert b["confirmed"] is False + return + pytest.skip("no shared unconfirmed tx between brk and mempool.space")