From 23ec99607e01306f5301c10d292f94719a090e96 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Thu, 27 Aug 2026 17:33:17 +0200 Subject: [PATCH] add detailed error message --- routstr/wallet.py | 17 +++++--- tests/unit/test_fee_payout_crash_safety.py | 42 +++++++++++++++++++ .../unit/test_lnurl_amount_and_destination.py | 12 +++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/routstr/wallet.py b/routstr/wallet.py index 0c0ecc7c..51ee7f72 100644 --- a/routstr/wallet.py +++ b/routstr/wallet.py @@ -2647,11 +2647,18 @@ async def periodic_routstr_fee_payout() -> None: logger.warning("Routstr fee payout was already claimed") continue except BaseException as e: - logger.critical( - "Routstr fee payout outcome is unknown; awaiting quote reconciliation", - extra={"payout_in_progress_msats": paid_msats}, - exc_info=isinstance(e, Exception), - ) + if attempt_quote_id is None: + logger.error( + "Routstr fee payout failed before melt dispatch", + extra={"payout_msats": paid_msats}, + exc_info=isinstance(e, Exception), + ) + else: + logger.critical( + "Routstr fee payout outcome is unknown; awaiting quote reconciliation", + extra={"payout_in_progress_msats": paid_msats}, + exc_info=isinstance(e, Exception), + ) if not isinstance(e, Exception): raise continue diff --git a/tests/unit/test_fee_payout_crash_safety.py b/tests/unit/test_fee_payout_crash_safety.py index 9e1dfaf2..c6fc3073 100644 --- a/tests/unit/test_fee_payout_crash_safety.py +++ b/tests/unit/test_fee_payout_crash_safety.py @@ -11,6 +11,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession from routstr import wallet from routstr.core import db +from routstr.payment.lnurl import LNURLError class _SessionContext: @@ -525,6 +526,47 @@ async def test_fee_payout_keeps_legacy_checkpoint_without_quote_locked() -> None critical.assert_called_once() +@pytest.mark.asyncio +async def test_fee_payout_failure_before_quote_is_not_reported_as_unknown() -> None: + session = Mock() + fee = SimpleNamespace( + accumulated_msats=1_061_000, + payout_in_progress_msats=0, + payout_started_at=None, + ) + reset = AsyncMock() + + with ( + patch("routstr.auth.ROUTSTR_FEE_DEFAULT_PAYOUT", 1), + patch("routstr.auth.ROUTSTR_FEE_PAYOUT_INTERVAL_SECONDS", 1), + patch("routstr.auth.ROUTSTR_LN_ADDRESS", "fees@example.com"), + patch( + "routstr.wallet.asyncio.sleep", + AsyncMock(side_effect=[None, asyncio.CancelledError()]), + ), + patch( + "routstr.wallet.db.create_session", return_value=_session_context(session) + ), + patch("routstr.wallet.db.get_routstr_fee", AsyncMock(return_value=fee)), + patch("routstr.wallet.db.reset_routstr_fee", reset), + patch("routstr.wallet.get_wallet", AsyncMock(return_value=Mock())), + patch("routstr.wallet.get_proofs_per_mint_and_unit", return_value=[]), + patch( + "routstr.wallet.raw_send_to_lnurl", + side_effect=LNURLError("Cashu melt fees leave no payable LNURL amount"), + ), + patch("routstr.wallet.logger.error") as error, + patch("routstr.wallet.logger.critical") as critical, + ): + with pytest.raises(asyncio.CancelledError): + await wallet.periodic_routstr_fee_payout() + + reset.assert_not_awaited() + critical.assert_not_called() + error.assert_called_once() + assert error.call_args.args[0] == "Routstr fee payout failed before melt dispatch" + + @pytest.mark.asyncio async def test_fee_payout_keeps_checkpoint_when_send_outcome_is_unknown() -> None: session = Mock() diff --git a/tests/unit/test_lnurl_amount_and_destination.py b/tests/unit/test_lnurl_amount_and_destination.py index e722141a..ab6a996e 100644 --- a/tests/unit/test_lnurl_amount_and_destination.py +++ b/tests/unit/test_lnurl_amount_and_destination.py @@ -347,21 +347,21 @@ async def test_send_to_lnurl_does_not_reserve_before_lnurl_validation() -> None: assert raw_send.await_args.kwargs["amount"] == 1000 -def test_select_melt_proofs_stops_at_minimal_cover_when_over_budget() -> None: +def test_select_melt_proofs_ignores_fees_for_unneeded_wallet_proofs() -> None: from routstr.payment.lnurl import _select_melt_proofs wallet = MagicMock() wallet.get_fees_for_proofs = MagicMock(side_effect=lambda selected: len(selected)) - proofs = [MagicMock(amount=600, reserved=False) for _ in range(3)] + proofs = [MagicMock(amount=2048, reserved=False) for _ in range(1100)] selected, shortfall = _select_melt_proofs( wallet, proofs, - quote_amount=1000, - fee_reserve=0, - gross_budget=1000, + quote_amount=1061, + fee_reserve=1, + gross_budget=1061, ) assert selected is None assert shortfall == 2 - assert wallet.get_fees_for_proofs.call_count == 2 + assert wallet.get_fees_for_proofs.call_count == 1