mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 14:24:47 -07:00
global: fixes
This commit is contained in:
@@ -9026,7 +9026,7 @@ impl BrkClient {
|
|||||||
|
|
||||||
/// Address transactions
|
/// 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)*
|
/// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -90,19 +90,20 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Esplora `/address/:address/txs` first page: up to `mempool_limit`
|
/// Esplora `/address/:address/txs` first page: up to `mempool_limit`
|
||||||
/// mempool (newest first) followed by the first `chain_limit`
|
/// mempool entries (newest first), then chain entries fill the response
|
||||||
/// confirmed. Pagination is path-style via `/txs/chain/:after_txid`.
|
/// up to `total_limit`. Pagination is path-style via `/txs/chain/:after_txid`.
|
||||||
pub fn addr_txs(
|
pub fn addr_txs(
|
||||||
&self,
|
&self,
|
||||||
addr: Addr,
|
addr: Addr,
|
||||||
|
total_limit: usize,
|
||||||
mempool_limit: usize,
|
mempool_limit: usize,
|
||||||
chain_limit: usize,
|
|
||||||
) -> Result<Vec<Transaction>> {
|
) -> Result<Vec<Transaction>> {
|
||||||
let mut out = if self.mempool().is_some() {
|
let mut out = if self.mempool().is_some() {
|
||||||
self.addr_mempool_txs(&addr, mempool_limit)?
|
self.addr_mempool_txs(&addr, mempool_limit)?
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
let chain_limit = total_limit.saturating_sub(out.len());
|
||||||
out.extend(self.addr_txs_chain(&addr, None, chain_limit)?);
|
out.extend(self.addr_txs_chain(&addr, None, chain_limit)?);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ impl Query {
|
|||||||
|
|
||||||
let max_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
|
let max_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
|
||||||
if height > max_height {
|
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()?;
|
let position = indexer.vecs.blocks.position.collect_one(height).data()?;
|
||||||
|
|||||||
@@ -13,25 +13,25 @@ use crate::Query;
|
|||||||
const RECENT_REPLACEMENTS_LIMIT: usize = 25;
|
const RECENT_REPLACEMENTS_LIMIT: usize = 25;
|
||||||
|
|
||||||
impl Query {
|
impl Query {
|
||||||
|
fn require_mempool(&self) -> Result<&Mempool> {
|
||||||
|
self.mempool().ok_or(Error::MempoolNotAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mempool_info(&self) -> Result<MempoolInfo> {
|
pub fn mempool_info(&self) -> Result<MempoolInfo> {
|
||||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
Ok(self.require_mempool()?.info())
|
||||||
Ok(mempool.info())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mempool_txids(&self) -> Result<Vec<Txid>> {
|
pub fn mempool_txids(&self) -> Result<Vec<Txid>> {
|
||||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
let txs = self.require_mempool()?.txs();
|
||||||
let txs = mempool.txs();
|
|
||||||
Ok(txs.keys().cloned().collect())
|
Ok(txs.keys().cloned().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recommended_fees(&self) -> Result<RecommendedFees> {
|
pub fn recommended_fees(&self) -> Result<RecommendedFees> {
|
||||||
self.mempool()
|
self.require_mempool().map(|m| m.fees())
|
||||||
.map(|mempool| mempool.fees())
|
|
||||||
.ok_or(Error::MempoolNotAvailable)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mempool_blocks(&self) -> Result<Vec<MempoolBlock>> {
|
pub fn mempool_blocks(&self) -> Result<Vec<MempoolBlock>> {
|
||||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
let mempool = self.require_mempool()?;
|
||||||
|
|
||||||
let block_stats = mempool.block_stats();
|
let block_stats = mempool.block_stats();
|
||||||
|
|
||||||
@@ -90,8 +90,7 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mempool_recent(&self) -> Result<Vec<MempoolRecentTx>> {
|
pub fn mempool_recent(&self) -> Result<Vec<MempoolRecentTx>> {
|
||||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
Ok(self.require_mempool()?.txs().recent().to_vec())
|
||||||
Ok(mempool.txs().recent().to_vec())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CPFP cluster for `txid`. Returns the mempool cluster when the txid is
|
/// 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`
|
/// walks `predecessors_of` backward to build the tree. `replaces`
|
||||||
/// is the requested tx's own direct predecessors.
|
/// is the requested tx's own direct predecessors.
|
||||||
pub fn tx_rbf(&self, txid: &Txid) -> Result<RbfResponse> {
|
pub fn tx_rbf(&self, txid: &Txid) -> Result<RbfResponse> {
|
||||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
let mempool = self.require_mempool()?;
|
||||||
let txs = mempool.txs();
|
let txs = mempool.txs();
|
||||||
let entries = mempool.entries();
|
let entries = mempool.entries();
|
||||||
let graveyard = mempool.graveyard();
|
let graveyard = mempool.graveyard();
|
||||||
@@ -422,7 +421,7 @@ impl Query {
|
|||||||
/// true, only trees with at least one non-signaling predecessor
|
/// true, only trees with at least one non-signaling predecessor
|
||||||
/// are returned.
|
/// are returned.
|
||||||
pub fn recent_replacements(&self, full_rbf_only: bool) -> Result<Vec<ReplacementNode>> {
|
pub fn recent_replacements(&self, full_rbf_only: bool) -> Result<Vec<ReplacementNode>> {
|
||||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
let mempool = self.require_mempool()?;
|
||||||
let txs = mempool.txs();
|
let txs = mempool.txs();
|
||||||
let entries = mempool.entries();
|
let entries = mempool.entries();
|
||||||
let graveyard = mempool.graveyard();
|
let graveyard = mempool.graveyard();
|
||||||
@@ -450,15 +449,17 @@ impl Query {
|
|||||||
.collect())
|
.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<Vec<u64>> {
|
pub fn transaction_times(&self, txids: &[Txid]) -> Result<Vec<u64>> {
|
||||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
let entries = self.require_mempool()?.entries();
|
||||||
let entries = mempool.entries();
|
|
||||||
Ok(txids
|
Ok(txids
|
||||||
.iter()
|
.iter()
|
||||||
.map(|txid| {
|
.map(|txid| {
|
||||||
entries
|
entries
|
||||||
.get(&TxidPrefix::from(txid))
|
.get(&TxidPrefix::from(txid))
|
||||||
.map(|e| usize::from(e.first_seen) as u64)
|
.map(|e| u64::from(e.first_seen))
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
|
|||||||
@@ -65,8 +65,17 @@ impl Query {
|
|||||||
TARGET_BLOCK_TIME
|
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
|
// 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()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
@@ -131,7 +140,7 @@ impl Query {
|
|||||||
previous_time,
|
previous_time,
|
||||||
next_retarget_height: Height::from(next_retarget_height),
|
next_retarget_height: Height::from(next_retarget_height),
|
||||||
time_avg: time_avg * 1000,
|
time_avg: time_avg * 1000,
|
||||||
adjusted_time_avg: time_avg * 1000,
|
adjusted_time_avg: adjusted_time_avg * 1000,
|
||||||
time_offset,
|
time_offset,
|
||||||
expected_blocks,
|
expected_blocks,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ impl AddrRoutes for ApiRouter<AppState> {
|
|||||||
State(state): State<AppState>
|
State(state): State<AppState>
|
||||||
| {
|
| {
|
||||||
let strategy = state.addr_strategy(Version::ONE, &path.addr, false);
|
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
|
}, |op| op
|
||||||
.id("get_address_txs")
|
.id("get_address_txs")
|
||||||
.addrs_tag()
|
.addrs_tag()
|
||||||
.summary("Address transactions")
|
.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::<Vec<Transaction>>()
|
.json_response::<Vec<Transaction>>()
|
||||||
.not_modified()
|
.not_modified()
|
||||||
.bad_request()
|
.bad_request()
|
||||||
|
|||||||
@@ -137,6 +137,13 @@ impl From<Timestamp> for usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Timestamp> for u64 {
|
||||||
|
#[inline]
|
||||||
|
fn from(value: Timestamp) -> Self {
|
||||||
|
u64::from(value.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Date> for Timestamp {
|
impl From<Date> for Timestamp {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(value: Date) -> Self {
|
fn from(value: Date) -> Self {
|
||||||
|
|||||||
@@ -10442,7 +10442,7 @@ class BrkClient extends BrkClientBase {
|
|||||||
/**
|
/**
|
||||||
* Address transactions
|
* 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)*
|
* *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7793,7 +7793,7 @@ class BrkClient(BrkClientBase):
|
|||||||
def get_address_txs(self, address: Addr) -> List[Transaction]:
|
def get_address_txs(self, address: Addr) -> List[Transaction]:
|
||||||
"""Address transactions.
|
"""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)*
|
*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*
|
||||||
|
|
||||||
|
|||||||
@@ -49,67 +49,44 @@ def test_address_txs_shape_dynamic(brk, mempool, live_addrs):
|
|||||||
|
|
||||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||||
def test_address_txs_ordering(brk, addr):
|
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)
|
b = brk.get_address_txs(addr)
|
||||||
if not b:
|
if not b:
|
||||||
pytest.skip(f"{addr} has no txs in brk")
|
pytest.skip(f"{addr} has no txs in brk")
|
||||||
for tx in b:
|
|
||||||
assert tx["status"]["confirmed"] is True, (
|
confirmed_flags = [tx["status"]["confirmed"] for tx in b]
|
||||||
f"{addr} returned unconfirmed tx {tx['txid']} (this endpoint is chain-only on brk)"
|
assert confirmed_flags == sorted(confirmed_flags), (
|
||||||
)
|
f"{addr}: confirmed flags must be False*..*True* (mempool prefix then chain), got "
|
||||||
heights = [tx["status"]["block_height"] for tx in b]
|
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), (
|
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)
|
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||||
def test_address_txs_limit(brk, addr):
|
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)
|
b = brk.get_address_txs(addr)
|
||||||
assert len(b) <= 50, f"{addr} returned {len(b)} txs, exceeds 50-cap"
|
assert len(b) <= 50, f"{addr} returned {len(b)} txs, exceeds 50-cap"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("addr", STABLE_ADDRS)
|
@pytest.mark.parametrize("addr", STABLE_ADDRS)
|
||||||
def test_address_txs_top_match_stable(brk, mempool, addr):
|
def test_address_txs_top_match_stable(brk, mempool, addr):
|
||||||
"""For inactive historical addresses, brk and mempool agree on first-page order."""
|
"""For inactive historical addresses, the confirmed tail must agree exactly with mempool.space."""
|
||||||
b_txids = [t["txid"] for t in brk.get_address_txs(addr)]
|
b_chain = [t["txid"] for t in brk.get_address_txs(addr) if t["status"]["confirmed"]]
|
||||||
m_txids = [t["txid"] for t in mempool.get_json(f"/api/address/{addr}/txs")]
|
m_chain = [
|
||||||
assert b_txids == m_txids, (
|
t["txid"]
|
||||||
f"{addr} first-page txid order diverges:\n"
|
for t in mempool.get_json(f"/api/address/{addr}/txs")
|
||||||
f" brk: {b_txids[:5]}...\n"
|
if t["status"]["confirmed"]
|
||||||
f" mempool: {m_txids[:5]}..."
|
]
|
||||||
)
|
assert b_chain == m_chain, (
|
||||||
|
f"{addr} confirmed-tail txid order diverges:\n"
|
||||||
|
f" brk: {b_chain[:5]}...\n"
|
||||||
def test_address_txs_pagination(brk, mempool):
|
f" mempool: {m_chain[:5]}..."
|
||||||
"""`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]}..."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ def test_difficulty_adjustment_invariants(brk):
|
|||||||
assert 1_000 <= d["timeAvg"] <= 3_600_000
|
assert 1_000 <= d["timeAvg"] <= 3_600_000
|
||||||
assert 1_000 <= d["adjustedTimeAvg"] <= 3_600_000
|
assert 1_000 <= d["adjustedTimeAvg"] <= 3_600_000
|
||||||
|
|
||||||
# remainingTime is constructed as remainingBlocks * timeAvg in brk.
|
# remainingTime is remainingBlocks * adjustedTimeAvg (matches mempool.space).
|
||||||
assert d["remainingTime"] == d["remainingBlocks"] * d["timeAvg"]
|
assert d["remainingTime"] == d["remainingBlocks"] * d["adjustedTimeAvg"]
|
||||||
|
|
||||||
assert d["estimatedRetargetDate"] > now_ms
|
assert d["estimatedRetargetDate"] > now_ms
|
||||||
assert d["previousTime"] * 1000 < now_ms
|
assert d["previousTime"] * 1000 < now_ms
|
||||||
|
|||||||
@@ -55,3 +55,26 @@ def test_cpfp_malformed_short(brk, bad):
|
|||||||
with pytest.raises(BrkError) as exc_info:
|
with pytest.raises(BrkError) as exc_info:
|
||||||
brk.get_text(f"/api/v1/cpfp/{bad}")
|
brk.get_text(f"/api/v1/cpfp/{bad}")
|
||||||
assert exc_info.value.status == 400
|
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")
|
||||||
|
|||||||
@@ -49,3 +49,29 @@ def test_tx_status_malformed_unknown(brk):
|
|||||||
with pytest.raises(BrkError) as exc_info:
|
with pytest.raises(BrkError) as exc_info:
|
||||||
brk.get_text(f"/api/tx/{bad}/status")
|
brk.get_text(f"/api/tx/{bad}/status")
|
||||||
assert exc_info.value.status == 404
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user