Merge pull request #699 from Routstr/add-detailed-error-message-for-payout

add detailed error message
This commit is contained in:
9qeklajc
2026-08-29 16:09:20 +02:00
committed by GitHub
3 changed files with 60 additions and 11 deletions
+12 -5
View File
@@ -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
@@ -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()
@@ -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