mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 14:54:47 -07:00
global: fixes
This commit is contained in:
@@ -2,47 +2,130 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
|
||||
|
||||
@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
|
||||
KNOWN_ADDR_TYPES = {
|
||||
"p2pk", "p2pkh", "p2sh", "v0_p2wpkh", "v0_p2wsh", "v1_p2tr",
|
||||
"multisig", "op_return", "p2a", "empty", "unknown",
|
||||
}
|
||||
|
||||
# Static fixtures: stable addresses with known shapes.
|
||||
STATIC_ADDRS = [
|
||||
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", # genesis coinbase, p2pkh — heavy path
|
||||
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # p2pkh — exercises tx_count divergence
|
||||
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # p2sh
|
||||
]
|
||||
|
||||
# Satoshi's genesis pubkey (uncompressed). Brk-only: mempool returns 400.
|
||||
SATOSHI_GENESIS_PUBKEY = (
|
||||
"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
def _tx_count_tolerance(m_tx_count: int) -> int:
|
||||
"""Allow drift between brk's distinct-tx and mempool's output-count semantics."""
|
||||
import math
|
||||
return max(5, math.ceil(0.05 * m_tx_count))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_info_shape(brk, mempool, addr):
|
||||
"""Typed brk response must structurally match mempool and echo the input address."""
|
||||
path = f"/api/address/{addr}"
|
||||
b = brk.get_address(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert b["address"] == m["address"]
|
||||
assert b["address"] == addr
|
||||
assert "addr_type" in b
|
||||
assert "type_index" in b["chain_stats"]
|
||||
assert "realized_price" in b["chain_stats"]
|
||||
|
||||
|
||||
def test_address_info_discovered(brk, mempool, live_addrs):
|
||||
"""Address stats structure must match for each discovered type."""
|
||||
def test_address_info_shape_dynamic(brk, mempool, live_addrs):
|
||||
"""Same shape contract over each live-discovered scriptpubkey type."""
|
||||
assert live_addrs, "no live addresses discovered"
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_address(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert b["address"] == m["address"]
|
||||
assert b["address"] == addr
|
||||
|
||||
|
||||
def test_address_chain_stats_close(brk, mempool, live_addrs):
|
||||
"""Chain stats values must be close for each discovered address."""
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_chain_stats_match(brk, mempool, addr):
|
||||
"""Funded/spent counts and sums must match exactly; tx_count tolerated within 5% (min 5)."""
|
||||
path = f"/api/address/{addr}"
|
||||
b = brk.get_address(addr)["chain_stats"]
|
||||
m = mempool.get_json(path)["chain_stats"]
|
||||
show("GET", f"{path} [chain_stats]", b, m)
|
||||
for key in ("funded_txo_count", "funded_txo_sum", "spent_txo_count", "spent_txo_sum"):
|
||||
assert b[key] == m[key], (
|
||||
f"{addr} {key}: brk={b[key]} vs mempool={m[key]}"
|
||||
)
|
||||
tol = _tx_count_tolerance(m["tx_count"])
|
||||
assert abs(b["tx_count"] - m["tx_count"]) <= tol, (
|
||||
f"{addr} tx_count drift {abs(b['tx_count'] - m['tx_count'])} > tol {tol}: "
|
||||
f"brk={b['tx_count']} vs mempool={m['tx_count']}"
|
||||
)
|
||||
|
||||
|
||||
def test_address_chain_stats_match_dynamic(brk, mempool, live_addrs):
|
||||
"""Same equality/tolerance contract on dynamically discovered addresses."""
|
||||
assert live_addrs, "no live addresses discovered"
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}"
|
||||
b = brk.get_json(path)["chain_stats"]
|
||||
b = brk.get_address(addr)["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']}"
|
||||
for key in ("funded_txo_count", "funded_txo_sum", "spent_txo_count", "spent_txo_sum"):
|
||||
assert b[key] == m[key], (
|
||||
f"{atype} {addr} {key}: brk={b[key]} vs mempool={m[key]}"
|
||||
)
|
||||
tol = _tx_count_tolerance(m["tx_count"])
|
||||
assert abs(b["tx_count"] - m["tx_count"]) <= tol, (
|
||||
f"{atype} {addr} tx_count drift {abs(b['tx_count'] - m['tx_count'])} > tol {tol}: "
|
||||
f"brk={b['tx_count']} vs mempool={m['tx_count']}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_brk_extras(brk, addr):
|
||||
"""Brk-only extras must be coherent: known addr_type, non-negative type_index/realized_price."""
|
||||
b = brk.get_address(addr)
|
||||
assert b["addr_type"] in KNOWN_ADDR_TYPES, (
|
||||
f"unknown addr_type {b['addr_type']!r} for {addr}"
|
||||
)
|
||||
cs = b["chain_stats"]
|
||||
assert cs["type_index"] >= 0, f"negative type_index for {addr}: {cs['type_index']}"
|
||||
assert cs["realized_price"] >= 0, (
|
||||
f"negative realized_price for {addr}: {cs['realized_price']}"
|
||||
)
|
||||
if cs["tx_count"] == 0:
|
||||
assert cs["realized_price"] == 0, (
|
||||
f"unfunded address {addr} must have realized_price=0, got {cs['realized_price']}"
|
||||
)
|
||||
|
||||
|
||||
def test_address_invalid(brk):
|
||||
"""Garbage input must produce a BrkError carrying HTTP 400."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_address("abc")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
|
||||
def test_address_pubkey_as_address(brk):
|
||||
"""Brk-only: hex-encoded pubkey is accepted as a P2PK address."""
|
||||
b = brk.get_address(SATOSHI_GENESIS_PUBKEY)
|
||||
assert b["addr_type"] == "p2pk", f"expected p2pk, got {b['addr_type']!r}"
|
||||
assert b["chain_stats"]["funded_txo_count"] >= 1, (
|
||||
f"genesis pubkey must have at least one funded output, got "
|
||||
f"{b['chain_stats']['funded_txo_count']}"
|
||||
)
|
||||
|
||||
@@ -2,33 +2,44 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S",
|
||||
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r",
|
||||
], ids=["p2pkh", "p2sh"])
|
||||
def static_addr(request):
|
||||
return request.param
|
||||
# Heavy address (recently active) — stresses the 50-cap path; cannot be ordered
|
||||
# exactly against mempool.space because the two indexers drift at the chain tip.
|
||||
ACTIVE_ADDR = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
||||
|
||||
# Inactive historical addresses — both indexers agree exactly on first-page
|
||||
# ordering and on pagination.
|
||||
STABLE_ADDRS = [
|
||||
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # p2pkh, ~125 txs
|
||||
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # p2sh, ~5700 txs (heavy pagination)
|
||||
]
|
||||
|
||||
STATIC_ADDRS = [ACTIVE_ADDR] + STABLE_ADDRS
|
||||
|
||||
|
||||
def test_address_txs_static(brk, mempool, static_addr):
|
||||
"""Confirmed+mempool tx list structure must match for well-known addresses."""
|
||||
path = f"/api/address/{static_addr}/txs"
|
||||
b = brk.get_json(path)
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_txs_shape(brk, mempool, addr):
|
||||
"""Typed list response must structurally match mempool; brk's `index` extra is allowed."""
|
||||
path = f"/api/address/{addr}/txs"
|
||||
b = brk.get_address_txs(addr)
|
||||
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])
|
||||
assert "index" in b[0], "brk-only `index` field missing"
|
||||
|
||||
|
||||
def test_address_txs_discovered(brk, mempool, live_addrs):
|
||||
"""Confirmed+mempool tx list structure must match for each discovered type."""
|
||||
def test_address_txs_shape_dynamic(brk, mempool, live_addrs):
|
||||
"""Same shape contract over each live-discovered scriptpubkey type."""
|
||||
assert live_addrs, "no live addresses discovered"
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}/txs"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_address_txs(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", f"({len(b)} txs)", f"({len(m)} txs)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
@@ -36,14 +47,76 @@ def test_address_txs_discovered(brk, mempool, live_addrs):
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
def test_address_txs_fields(brk, mempool, live):
|
||||
"""Every tx in the list must carry the core mempool.space fields."""
|
||||
path = f"/api/address/{live.sample_address}/txs"
|
||||
b = brk.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", "—")
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_txs_ordering(brk, addr):
|
||||
"""All entries must be confirmed and heights monotonically non-increasing."""
|
||||
b = brk.get_address_txs(addr)
|
||||
if not b:
|
||||
pytest.skip("address has no txs in brk")
|
||||
required = {"txid", "version", "locktime", "vin", "vout", "size", "weight", "fee", "status"}
|
||||
for tx in b[:5]:
|
||||
missing = required - set(tx.keys())
|
||||
assert not missing, f"tx {tx.get('txid', '?')} missing fields: {missing}"
|
||||
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]
|
||||
assert heights == sorted(heights, reverse=True), (
|
||||
f"{addr} 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."""
|
||||
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]}..."
|
||||
)
|
||||
|
||||
|
||||
def test_address_txs_invalid(brk):
|
||||
"""Garbage input must produce a BrkError carrying HTTP 400."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_address_txs("abc")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
"""GET /api/address/{address}/txs/chain"""
|
||||
"""GET /api/address/{address}/txs/chain (and /txs/chain/{after_txid})"""
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S",
|
||||
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r",
|
||||
], ids=["p2pkh", "p2sh"])
|
||||
def static_addr(request):
|
||||
return request.param
|
||||
# Heavy active address (chain-tip drift expected, no exact-order assertion)
|
||||
ACTIVE_ADDR = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
||||
|
||||
# Inactive historical addresses — both indexers agree exactly on first-page ordering
|
||||
STABLE_ADDRS = [
|
||||
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # p2pkh, ~125 txs
|
||||
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # p2sh, ~5700 txs (heavy pagination)
|
||||
]
|
||||
|
||||
STATIC_ADDRS = [ACTIVE_ADDR] + STABLE_ADDRS
|
||||
|
||||
|
||||
def test_address_txs_chain_static(brk, mempool, static_addr):
|
||||
"""Confirmed-only tx list structure must match for well-known addresses."""
|
||||
path = f"/api/address/{static_addr}/txs/chain"
|
||||
b = brk.get_json(path)
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_txs_chain_shape(brk, mempool, addr):
|
||||
"""Typed list response must structurally match mempool; brk's `index` extra is allowed."""
|
||||
path = f"/api/address/{addr}/txs/chain"
|
||||
b = brk.get_address_confirmed_txs(addr)
|
||||
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])
|
||||
assert "index" in b[0], "brk-only `index` field missing"
|
||||
|
||||
|
||||
def test_address_txs_chain_discovered(brk, mempool, live_addrs):
|
||||
"""Confirmed-only tx list structure must match for each discovered type."""
|
||||
def test_address_txs_chain_shape_dynamic(brk, mempool, live_addrs):
|
||||
"""Same shape contract over each live-discovered scriptpubkey type."""
|
||||
assert live_addrs, "no live addresses discovered"
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}/txs/chain"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_address_confirmed_txs(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", f"({len(b)} txs)", f"({len(m)} txs)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
@@ -36,14 +45,88 @@ def test_address_txs_chain_discovered(brk, mempool, live_addrs):
|
||||
assert_same_structure(b[0], m[0])
|
||||
|
||||
|
||||
def test_address_txs_chain_all_confirmed(brk, live):
|
||||
"""Every tx returned by /txs/chain must have confirmed=True in its status."""
|
||||
path = f"/api/address/{live.sample_address}/txs/chain"
|
||||
b = brk.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", "—")
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_txs_chain_all_confirmed(brk, addr):
|
||||
"""Every entry must have `status.confirmed == True`."""
|
||||
b = brk.get_address_confirmed_txs(addr)
|
||||
if not b:
|
||||
pytest.skip("address has no confirmed txs in brk")
|
||||
unconfirmed = [t for t in b if not t.get("status", {}).get("confirmed", False)]
|
||||
pytest.skip(f"{addr} has no confirmed txs in brk")
|
||||
unconfirmed = [t for t in b if not t["status"]["confirmed"]]
|
||||
assert not unconfirmed, (
|
||||
f"{len(unconfirmed)} unconfirmed tx(s) returned by /txs/chain"
|
||||
f"{addr}: {len(unconfirmed)} unconfirmed tx(s) returned: "
|
||||
f"{[t['txid'] for t in unconfirmed[:3]]}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_txs_chain_ordering(brk, addr):
|
||||
"""Heights must be monotonically non-increasing (newest first)."""
|
||||
b = brk.get_address_confirmed_txs(addr)
|
||||
if not b:
|
||||
pytest.skip(f"{addr} has no confirmed txs in brk")
|
||||
heights = [t["status"]["block_height"] for t in b]
|
||||
assert heights == sorted(heights, reverse=True), (
|
||||
f"{addr} not newest-first by height: {heights[:5]}..."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("addr", STATIC_ADDRS)
|
||||
def test_address_txs_chain_limit(brk, addr):
|
||||
"""Hard cap of 25 confirmed txs per call."""
|
||||
b = brk.get_address_confirmed_txs(addr)
|
||||
assert len(b) <= 25, f"{addr} returned {len(b)} txs, exceeds 25-cap"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("addr", STABLE_ADDRS)
|
||||
def test_address_txs_chain_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_confirmed_txs(addr)]
|
||||
m_txids = [t["txid"] for t in mempool.get_json(f"/api/address/{addr}/txs/chain")]
|
||||
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_chain_pagination(brk, mempool):
|
||||
"""Path-style pagination must match mempool.space's Esplora-canonical form exactly."""
|
||||
addr = "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r"
|
||||
first = brk.get_address_confirmed_txs(addr)
|
||||
assert len(first) == 25, f"expected full first page (25), got {len(first)}"
|
||||
last_txid = first[-1]["txid"]
|
||||
last_height = first[-1]["status"]["block_height"]
|
||||
|
||||
second = brk.get_address_confirmed_txs_after(addr, last_txid)
|
||||
assert second, "second page must be non-empty for a 5700-tx address"
|
||||
assert len(second) <= 25, f"page 2 exceeds 25-cap: {len(second)}"
|
||||
|
||||
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"]["confirmed"] is True, f"page 2 has unconfirmed tx {tx['txid']}"
|
||||
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}"
|
||||
)
|
||||
|
||||
# Cross-check against mempool.space's path-style form.
|
||||
m_second = mempool.get_json(f"/api/address/{addr}/txs/chain/{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 path-style:\n"
|
||||
f" brk: {b_ids[:5]}...\n"
|
||||
f" mempool: {m_ids[:5]}..."
|
||||
)
|
||||
|
||||
|
||||
def test_address_txs_chain_invalid(brk):
|
||||
"""Garbage input must produce a BrkError carrying HTTP 400."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_address_confirmed_txs("abc")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -1,33 +1,55 @@
|
||||
"""GET /api/address/{address}/txs/mempool"""
|
||||
|
||||
from _lib import show
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
|
||||
|
||||
def test_address_txs_mempool_sample(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)
|
||||
|
||||
|
||||
def test_address_txs_mempool_discovered(brk, mempool, live_addrs):
|
||||
"""Mempool tx list must be a (possibly empty) array for each discovered type."""
|
||||
def test_address_txs_mempool_shape_dynamic(brk, mempool, live_addrs):
|
||||
"""Shape contract over each live-discovered scriptpubkey type."""
|
||||
assert live_addrs, "no live addresses discovered"
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}/txs/mempool"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_address_mempool_txs(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", 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])
|
||||
|
||||
|
||||
def test_address_txs_mempool_all_unconfirmed(brk, live):
|
||||
"""Every tx returned by /txs/mempool must have confirmed=False (if any)."""
|
||||
path = f"/api/address/{live.sample_address}/txs/mempool"
|
||||
b = brk.get_json(path)
|
||||
show("GET", path, f"({len(b)} txs)", "—")
|
||||
confirmed = [t for t in b if t.get("status", {}).get("confirmed", False)]
|
||||
assert not confirmed, (
|
||||
f"{len(confirmed)} confirmed tx(s) returned by /txs/mempool"
|
||||
def test_address_txs_mempool_limit(brk, live_addrs):
|
||||
"""Hard cap of 50 mempool txs per call."""
|
||||
for _atype, addr in live_addrs:
|
||||
b = brk.get_address_mempool_txs(addr)
|
||||
assert len(b) <= 50, f"{addr} returned {len(b)} txs, exceeds 50-cap"
|
||||
|
||||
|
||||
def test_address_txs_mempool_all_unconfirmed(brk, live_addrs):
|
||||
"""Every entry must have status.confirmed == False."""
|
||||
for _atype, addr in live_addrs:
|
||||
b = brk.get_address_mempool_txs(addr)
|
||||
confirmed = [t for t in b if t["status"]["confirmed"]]
|
||||
assert not confirmed, (
|
||||
f"{addr}: {len(confirmed)} confirmed tx(s) returned by /txs/mempool: "
|
||||
f"{[t['txid'] for t in confirmed[:3]]}"
|
||||
)
|
||||
|
||||
|
||||
def test_address_txs_mempool_unique_txids(brk, live_addrs):
|
||||
"""No duplicate txids within a single response."""
|
||||
for _atype, addr in live_addrs:
|
||||
b = brk.get_address_mempool_txs(addr)
|
||||
txids = [t["txid"] for t in b]
|
||||
assert len(txids) == len(set(txids)), f"{addr}: duplicate txids in response"
|
||||
|
||||
|
||||
def test_address_txs_mempool_invalid(brk):
|
||||
"""Garbage input must produce a BrkError carrying HTTP 400."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_address_mempool_txs("abc")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,51 +2,70 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_values, show
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S",
|
||||
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r",
|
||||
], ids=["p2pkh", "p2sh"])
|
||||
def static_addr(request):
|
||||
return request.param
|
||||
# Inactive historical addresses with stable, comparable UTXO sets.
|
||||
STABLE_ADDRS = [
|
||||
("p2pkh", "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S"),
|
||||
("p2sh", "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r"),
|
||||
]
|
||||
|
||||
# Genesis pubkey-hash address: tens of thousands of dust UTXOs — exceeds both
|
||||
# brk's 1000-cap and mempool.space's 500-cap, so both indexers must 400.
|
||||
HEAVY_ADDR = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
||||
|
||||
|
||||
def test_address_utxo_static(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)
|
||||
@pytest.mark.parametrize("atype,addr", STABLE_ADDRS, ids=[a for a, _ in STABLE_ADDRS])
|
||||
def test_address_utxo_static(brk, mempool, atype, addr):
|
||||
"""Exact UTXO parity (txid+vout+value+status) for stable historical addresses."""
|
||||
path = f"/api/address/{addr}/utxo"
|
||||
b = brk.get_address_utxos(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} utxos)", f"({len(m)} utxos)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
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)
|
||||
show("GET", f"{path} [{atype}]", f"({len(b)} utxos)", f"({len(m)} utxos)")
|
||||
key = lambda u: (u["txid"], u["vout"])
|
||||
assert_same_values(sorted(b, key=key), sorted(m, key=key))
|
||||
|
||||
|
||||
def test_address_utxo_discovered(brk, mempool, live_addrs):
|
||||
"""UTXO list must match for each discovered address type — same txids, values, and statuses."""
|
||||
"""Same exact-parity contract over each live-discovered scriptpubkey type."""
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/address/{addr}/utxo"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_address_utxos(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", f"({len(b)} utxos)", f"({len(m)} utxos)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
key = lambda u: (u.get("txid", ""), u.get("vout", 0))
|
||||
key = lambda u: (u["txid"], u["vout"])
|
||||
assert_same_values(sorted(b, key=key), sorted(m, key=key))
|
||||
|
||||
|
||||
def test_address_utxo_fields(brk, live):
|
||||
"""Every utxo must carry the core mempool.space fields."""
|
||||
path = f"/api/address/{live.sample_address}/utxo"
|
||||
b = brk.get_json(path)
|
||||
show("GET", path, f"({len(b)} utxos)", "—")
|
||||
@pytest.mark.parametrize("atype,addr", STABLE_ADDRS, ids=[a for a, _ in STABLE_ADDRS])
|
||||
def test_address_utxo_all_confirmed(brk, atype, addr):
|
||||
"""brk's /utxo only returns confirmed UTXOs (mempool-funded ones are excluded by design)."""
|
||||
b = brk.get_address_utxos(addr)
|
||||
if not b:
|
||||
pytest.skip("address has no utxos in brk")
|
||||
required = {"txid", "vout", "value", "status"}
|
||||
for u in b[:5]:
|
||||
missing = required - set(u.keys())
|
||||
assert not missing, f"utxo {u.get('txid', '?')}:{u.get('vout', '?')} missing fields: {missing}"
|
||||
assert isinstance(u["value"], int) and u["value"] > 0
|
||||
pytest.skip(f"{addr} has no utxos in brk")
|
||||
unconfirmed = [u for u in b if not u["status"]["confirmed"]]
|
||||
assert not unconfirmed, (
|
||||
f"{addr}: {len(unconfirmed)} unconfirmed UTXO(s) returned: "
|
||||
f"{[(u['txid'], u['vout']) for u in unconfirmed[:3]]}"
|
||||
)
|
||||
|
||||
|
||||
def test_address_utxo_too_many(brk):
|
||||
"""Heavy address (>1000 UTXOs) must produce BrkError(status=400, code=too_many_utxos)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_address_utxos(HEAVY_ADDR)
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
|
||||
def test_address_utxo_invalid(brk):
|
||||
"""Garbage input must produce a BrkError carrying HTTP 400."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_address_utxos("abc")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -5,49 +5,79 @@ import pytest
|
||||
from _lib import assert_same_structure, assert_same_values, show
|
||||
|
||||
|
||||
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
|
||||
VALID_ADDRS = [
|
||||
("p2pkh-genesis", "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"),
|
||||
("p2sh", "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"),
|
||||
("p2wpkh", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"),
|
||||
("p2wsh", "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"),
|
||||
("p2tr", "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"),
|
||||
]
|
||||
|
||||
INVALID_ADDRS = [
|
||||
("garbage", "notanaddress123"),
|
||||
("bad-checksum-p2pkh", "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb"),
|
||||
("bad-checksum-p2sh", "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLz"),
|
||||
("bad-checksum-p2wpkh", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5"),
|
||||
("wrong-network-bech32", "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"),
|
||||
("mixed-case-bech32", "bc1QRP33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"),
|
||||
]
|
||||
|
||||
# Satoshi's genesis-coinbase pubkey: brk validates this as p2pk; mempool.space
|
||||
# rejects all raw-pubkey-hex inputs. Documents the intentional brk superset.
|
||||
GENESIS_PUBKEY = (
|
||||
"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb6"
|
||||
"49f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("addr,kind", [
|
||||
("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "p2pkh-genesis"),
|
||||
("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", "p2sh"),
|
||||
("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", "p2wpkh"),
|
||||
("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", "p2tr"),
|
||||
])
|
||||
def test_validate_address_static_valid(brk, mempool, addr, kind):
|
||||
@pytest.mark.parametrize("kind,addr", VALID_ADDRS, ids=[k for k, _ in VALID_ADDRS])
|
||||
def test_validate_address_static_valid(brk, mempool, kind, addr):
|
||||
"""Well-known addresses across all script types must validate identically."""
|
||||
path = f"/api/v1/validate-address/{addr}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.validate_address(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{kind}]", b, m)
|
||||
assert_same_values(b, m)
|
||||
assert b["isvalid"] is True
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("addr,kind", [
|
||||
("notanaddress123", "garbage"),
|
||||
("", "empty"),
|
||||
("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb", "bad-checksum-p2pkh"),
|
||||
("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", "bad-checksum-p2wpkh"),
|
||||
("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLz", "bad-checksum-p2sh"),
|
||||
])
|
||||
def test_validate_address_invalid(brk, mempool, addr, kind):
|
||||
"""Invalid addresses must produce the same rejection structure."""
|
||||
def test_validate_address_discovered(brk, mempool, live_addrs):
|
||||
"""Validation of each live-discovered scriptpubkey type must match exactly."""
|
||||
for atype, addr in live_addrs:
|
||||
path = f"/api/v1/validate-address/{addr}"
|
||||
b = brk.validate_address(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{atype}]", b, m)
|
||||
assert b["isvalid"] is True
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind,addr", INVALID_ADDRS, ids=[k for k, _ in INVALID_ADDRS])
|
||||
def test_validate_address_invalid(brk, mempool, kind, addr):
|
||||
"""Invalid addresses produce isvalid=false; structure must match (error strings differ by impl)."""
|
||||
path = f"/api/v1/validate-address/{addr}"
|
||||
if kind == "empty":
|
||||
# An empty path segment routes to a different endpoint — skip.
|
||||
pytest.skip("empty address routes to a different endpoint")
|
||||
b = brk.get_json(path)
|
||||
b = brk.validate_address(addr)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", f"{path} [{kind}]", b, m)
|
||||
assert b["isvalid"] is False
|
||||
assert m["isvalid"] is False
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_validate_address_pubkey_hex_brk_only(brk, mempool):
|
||||
"""Raw pubkey hex: brk accepts as p2pk (superset); mempool.space rejects (non-2xx or no isvalid:true)."""
|
||||
path = f"/api/v1/validate-address/{GENESIS_PUBKEY}"
|
||||
b = brk.validate_address(GENESIS_PUBKEY)
|
||||
m_resp = mempool.get_raw(path)
|
||||
show("GET", path, b, f"<HTTP {m_resp.status_code}> {m_resp.text[:200]}")
|
||||
assert b["isvalid"] is True, "brk must accept raw pubkey hex as p2pk"
|
||||
assert b.get("isscript") is False
|
||||
assert b.get("iswitness") is False
|
||||
if 200 <= m_resp.status_code < 300:
|
||||
try:
|
||||
m = m_resp.json()
|
||||
except ValueError:
|
||||
m = None
|
||||
assert not (isinstance(m, dict) and m.get("isvalid") is True), (
|
||||
"mempool.space must not validate raw pubkey hex as a real address"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user