git / moonbite-network / tests/test_chain_address_maturity.py · raw

"""/api/chain/address must keep mining rewards younger than COINBASE_MATURITYout of the spendable balance and the spendable UTXO list, so wallets neverbuild a transaction the node rejects as a premature coinbase spend."""import pytestimport web_appTIP = 1000ADDR = "moon1qmaturitytest"class _Rpc:    """scantxoutset: three coins. gettxout: which are coinbase."""    def __init__(self, gettxout_fails=False):        self.gettxout_calls = []        self.gettxout_fails = gettxout_fails    def scantxoutset(self, *_a):        return {            "success": True,            "height": TIP,            "unspents": [                # old mining reward: mature, no gettxout needed                {"txid": "aa" * 32, "vout": 0, "amount": 10, "height": TIP - 500},                # fresh mining reward: still maturing                {"txid": "bb" * 32, "vout": 0, "amount": 10, "height": TIP - 5},                # fresh ordinary payment: spendable straight away                {"txid": "cc" * 32, "vout": 1, "amount": 2.5, "height": TIP - 5},            ],        }    def gettxout(self, txid, vout, _mempool):        self.gettxout_calls.append(txid)        if self.gettxout_fails:            raise RuntimeError("rpc down")        return {"coinbase": txid == "bb" * 32, "value": 10}@pytest.fixture()def rpc(monkeypatch):    fake = _Rpc()    monkeypatch.setattr(web_app, "_merchant_use_rpc", lambda: True)    monkeypatch.setattr(web_app, "_valid_receiving_address", lambda a: True)    monkeypatch.setattr(web_app, "_get_merchant_rpc", lambda: fake)    monkeypatch.setattr(web_app, "_budgeted_scantxoutset", lambda r, a: r.scantxoutset())    web_app._chain_addr_cache.clear()    web_app.app.config["TESTING"] = True    return fakedef test_maturing_reward_is_not_spendable(rpc):    j = web_app.app.test_client().get(f"/api/chain/address/{ADDR}").get_json()    assert j["balance_units"] == 1_250_000_000          # 10 + 2.5 spendable    assert j["immature_units"] == 1_000_000_000         # the fresh reward    assert j["total_units"] == 2_250_000_000    assert {u["txid"] for u in j["utxos"]} == {"aa" * 32, "cc" * 32}    assert [u["txid"] for u in j["immature_utxos"]] == ["bb" * 32]    assert j["immature_utxos"][0]["matures_at_height"] == TIP - 5 + 99    # only the two recent coins needed a coinbase lookup    assert sorted(rpc.gettxout_calls) == ["bb" * 32, "cc" * 32]def test_reward_becomes_spendable_at_maturity(rpc, monkeypatch):    # coin at height h is spendable in block h+100, i.e. once the tip is h+99    orig = rpc.scantxoutset    def at_maturity(*_a):        res = orig()        res["height"] = (TIP - 5) + 99        return res    monkeypatch.setattr(rpc, "scantxoutset", at_maturity)    j = web_app.app.test_client().get(f"/api/chain/address/{ADDR}").get_json()    assert j["immature_units"] == 0    assert j["balance_units"] == j["total_units"] == 2_250_000_000def test_unknown_coinbase_status_is_treated_as_maturing(monkeypatch):    fake = _Rpc(gettxout_fails=True)    monkeypatch.setattr(web_app, "_merchant_use_rpc", lambda: True)    monkeypatch.setattr(web_app, "_valid_receiving_address", lambda a: True)    monkeypatch.setattr(web_app, "_get_merchant_rpc", lambda: fake)    monkeypatch.setattr(web_app, "_budgeted_scantxoutset", lambda r, a: r.scantxoutset())    web_app._chain_addr_cache.clear()    j = web_app.app.test_client().get(f"/api/chain/address/{ADDR}").get_json()    assert [u["txid"] for u in j["utxos"]] == ["aa" * 32]    assert j["balance_units"] == 1_000_000_000