From ff9c645bb1f83eac8f4a8ecb14770d10c6e95414 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Fri, 19 Jun 2026 11:20:56 +0200 Subject: [PATCH] make sure balance is set correctly --- routstr/wallet.py | 10 +++- tests/unit/test_fetch_all_balances.py | 73 +++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_fetch_all_balances.py diff --git a/routstr/wallet.py b/routstr/wallet.py index 815da134..30f36ded 100644 --- a/routstr/wallet.py +++ b/routstr/wallet.py @@ -580,11 +580,19 @@ async def fetch_all_balances( } return error_result + # Build the set of mints to inspect. Received tokens are stored against + # ``primary_mint`` (which defaults to a real mint even when ``cashu_mints`` + # is empty), so include it as a fallback — otherwise a node that accepts + # payments would still report empty balances when ``cashu_mints`` is unset. + mint_urls: list[str] = list(settings.cashu_mints) + if settings.primary_mint and settings.primary_mint not in mint_urls: + mint_urls.append(settings.primary_mint) + # Create tasks for all mint/unit combinations async with db.create_session() as session: tasks = [ fetch_balance(session, mint_url, unit) - for mint_url in settings.cashu_mints + for mint_url in mint_urls for unit in units ] diff --git a/tests/unit/test_fetch_all_balances.py b/tests/unit/test_fetch_all_balances.py new file mode 100644 index 00000000..dcd99107 --- /dev/null +++ b/tests/unit/test_fetch_all_balances.py @@ -0,0 +1,73 @@ +from contextlib import asynccontextmanager +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from routstr.wallet import fetch_all_balances + + +@asynccontextmanager +async def _fake_session(): # type: ignore[no-untyped-def] + yield MagicMock() + + +def _patches(proof_amount: int = 1000): # type: ignore[no-untyped-def] + proof = MagicMock(amount=proof_amount) + return [ + patch("routstr.wallet.get_wallet", AsyncMock(return_value=MagicMock())), + patch( + "routstr.wallet.get_proofs_per_mint_and_unit", + MagicMock(return_value=[proof]), + ), + patch( + "routstr.wallet.slow_filter_spend_proofs", + AsyncMock(side_effect=lambda proofs, wallet: proofs), + ), + patch( + "routstr.wallet.db.balances_for_mint_and_unit", + AsyncMock(return_value=0), + ), + patch("routstr.wallet.db.create_session", _fake_session), + ] + + +@pytest.mark.asyncio +async def test_fetch_all_balances_falls_back_to_primary_mint() -> None: + """With empty cashu_mints, balances are still fetched for primary_mint.""" + from routstr.core.settings import settings + + with patch.object(settings, "cashu_mints", []), patch.object( + settings, "primary_mint", "http://primary:3338" + ): + for p in _patches(proof_amount=1000): + p.start() + try: + details, total_wallet, total_user, owner = await fetch_all_balances( + units=["sat"] + ) + finally: + patch.stopall() + + assert [d["mint_url"] for d in details] == ["http://primary:3338"] + assert total_wallet == 1000 + + +@pytest.mark.asyncio +async def test_fetch_all_balances_no_duplicate_primary_mint() -> None: + """primary_mint already in cashu_mints is not inspected twice.""" + from routstr.core.settings import settings + + with patch.object( + settings, "cashu_mints", ["http://primary:3338"] + ), patch.object(settings, "primary_mint", "http://primary:3338"): + for p in _patches(proof_amount=1000): + p.start() + try: + details, total_wallet, _total_user, _owner = await fetch_all_balances( + units=["sat"] + ) + finally: + patch.stopall() + + assert [d["mint_url"] for d in details] == ["http://primary:3338"] + assert total_wallet == 1000