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

"""Tests for the non-custodial order book — focus on cancel-token authorization.The cancel path must NOT trust the MBITE address (it is public in the orderbook); only the secret cancel_token minted at creation may cancel an order."""import importlibimport pytestimport exchange@pytest.fixture()def fresh_db(tmp_path):    """Point the exchange module at an isolated, empty SQLite file per test."""    exchange._conn = None    exchange._DB_PATH = tmp_path / "exchange.db"    yield    if exchange._conn is not None:        exchange._conn.close()    exchange._conn = None    importlib.reload(exchange)  # restore the module's real DB path for other testsdef _make_order():    return exchange.create_order(        side="sell",        pair="MBITE/LTC",        price="0.001",        amount="10",        mbite_address="moon1makeraddress",        quote_address="ltc1quoteaddress",    )def test_create_returns_cancel_token(fresh_db):    order = _make_order()    assert order["cancel_token"]    assert len(order["cancel_token"]) >= 20def test_public_reads_never_leak_token(fresh_db):    order = _make_order()    oid = order["id"]    # get_order (public GET path)    assert "cancel_token" not in exchange.get_order(oid)    # order book listing    book = exchange.list_orders(pair="MBITE/LTC")    assert all("cancel_token" not in row for row in book["asks"])    # settle hint    assert "cancel_token" not in exchange.settle_hint(oid)["order"]def test_cancel_with_correct_token_succeeds(fresh_db):    order = _make_order()    result = exchange.cancel_order(order["id"], order["cancel_token"])    assert result["status"] == "cancelled"def test_cancel_with_public_address_is_rejected(fresh_db):    """The old, broken auth: the public MBITE address must NOT cancel an order."""    order = _make_order()    with pytest.raises(ValueError, match="cancel token"):        exchange.cancel_order(order["id"], "moon1makeraddress")    assert exchange.get_order(order["id"])["status"] == "open"@pytest.mark.parametrize("bad", ["NaN", "nan", "Infinity", "-Infinity", "inf", "1e400"])def test_create_rejects_non_finite_or_huge_amounts(fresh_db, bad):    """Non-finite / absurd Decimals must raise ValueError, not crash with    decimal.InvalidOperation (which surfaced as an HTTP 500 in production)."""    with pytest.raises(ValueError):        exchange.create_order(            side="buy", pair="MBITE/LTC", price=bad, amount="1",            mbite_address="moon1x", quote_address="ltc1x",        )    with pytest.raises(ValueError):        exchange.create_order(            side="buy", pair="MBITE/LTC", price="0.001", amount=bad,            mbite_address="moon1x", quote_address="ltc1x",        )def test_cancel_with_wrong_or_empty_token_is_rejected(fresh_db):    order = _make_order()    with pytest.raises(ValueError, match="cancel token"):        exchange.cancel_order(order["id"], "not-the-token")    with pytest.raises(ValueError, match="cancel token"):        exchange.cancel_order(order["id"], "")    assert exchange.get_order(order["id"])["status"] == "open"# --------------------------------------------------------------------------- ## Phase 2a: atomic-swap settlement coordination (plumbing only, no on-chain# verification). A swap may progress no further than 'both_locked' and NO trade# is ever marked settled here, so last_price must stay null.# --------------------------------------------------------------------------- #_H = "a" * 64  # a fake 32-byte SHA256 hashlock (64 hex chars)_PK = "02" + "b" * 64  # a fake 33-byte compressed pubkey (66 hex chars)def _matched_pair():    """Create a crossing sell + buy from different addresses; both go 'matched'."""    sell = exchange.create_order(        side="sell", pair="MBITE/LTC", price="0.0005", amount="100",        mbite_address="moon1seller0001", quote_address="ltc1seller0001",    )    buy = exchange.create_order(        side="buy", pair="MBITE/LTC", price="0.0006", amount="100",        mbite_address="moon1buyer0001", quote_address="ltc1buyer0001",    )    assert buy["status"] == "matched"    return sell, buydef _init(order, cancel_token, **overrides):    params = dict(        hashlock=_H,        base_recipient_pk=_PK, base_refund_pk=_PK,        quote_recipient_pk=_PK, quote_refund_pk=_PK,        base_locktime=48 * 3600, quote_locktime=24 * 3600,    )    params.update(overrides)    return exchange.init_swap(order["id"], cancel_token, **params)def test_init_swap_creates_swap_in_init_state(fresh_db):    sell, buy = _matched_pair()    swap = _init(buy, buy["cancel_token"])    assert swap["status"] == "swap_init"    assert swap["buy_order_id"] == buy["id"]    assert swap["sell_order_id"] == sell["id"]    assert swap["hashlock"] == _H    assert swap["price"] == "0.0005"  # maker (resting sell) price    assert swap["base_htlc_txid"] is None and swap["quote_htlc_txid"] is None    assert swap["settled_at"] is None  # nothing settles in 2adef test_init_swap_requires_matched_order(fresh_db):    order = _make_order()  # open, not matched    with pytest.raises(ValueError, match="not matched"):        _init(order, order["cancel_token"])def test_init_swap_rejects_bad_cancel_token(fresh_db):    _sell, buy = _matched_pair()    with pytest.raises(ValueError, match="cancel token"):        _init(buy, "wrong-token")def test_init_swap_enforces_timelock_ordering(fresh_db):    _sell, buy = _matched_pair()    with pytest.raises(ValueError, match="quote_locktime must be earlier"):        _init(buy, buy["cancel_token"], base_locktime=1000, quote_locktime=2000)def test_init_swap_enforces_timelock_gap(fresh_db):    _sell, buy = _matched_pair()    # quote < base but gap under MIN_TIMELOCK_GAP_SECONDS    with pytest.raises(ValueError, match="timelock gap too small"):        _init(buy, buy["cancel_token"], base_locktime=1000, quote_locktime=999)def test_init_swap_rejects_duplicate(fresh_db):    sell, buy = _matched_pair()    _init(buy, buy["cancel_token"])    # Second init on either side of the same pair is refused.    with pytest.raises(ValueError, match="already exists"):        _init(sell, sell["cancel_token"], hashlock="c" * 64)def test_report_funding_advances_state_machine(fresh_db):    _sell, buy = _matched_pair()    _init(buy, buy["cancel_token"])    after_base = exchange.report_funding(        buy["id"], buy["cancel_token"], "base", "d" * 64    )    assert after_base["status"] == "base_funded"    after_quote = exchange.report_funding(        buy["id"], buy["cancel_token"], "quote", "e" * 64    )    assert after_quote["status"] == "both_locked"    assert after_quote["base_htlc_txid"] == "d" * 64    assert after_quote["quote_htlc_txid"] == "e" * 64def test_report_funding_requires_existing_swap(fresh_db):    _sell, buy = _matched_pair()    with pytest.raises(ValueError, match="no swap"):        exchange.report_funding(buy["id"], buy["cancel_token"], "base", "d" * 64)def test_swap_never_settles_so_last_price_stays_null(fresh_db):    _sell, buy = _matched_pair()    _init(buy, buy["cancel_token"])    exchange.report_funding(buy["id"], buy["cancel_token"], "base", "d" * 64)    exchange.report_funding(buy["id"], buy["cancel_token"], "quote", "e" * 64)    # Even fully "locked", no settlement occurs in Phase 2a: price stays null.    assert exchange.list_orders(pair="MBITE/LTC")["last_price"] is None    assert exchange.get_swap(buy["id"])["status"] == "both_locked"