From 27f53948cab846124c5412675d7e504f52013377 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Tue, 4 Aug 2026 22:59:50 +0200 Subject: [PATCH] clean up --- routstr/lightning.py | 32 ++++++++++------- tests/unit/test_lightning_settlement.py | 48 +++++++++++++++++++++---- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/routstr/lightning.py b/routstr/lightning.py index c8668232..6ef85f3c 100644 --- a/routstr/lightning.py +++ b/routstr/lightning.py @@ -456,11 +456,20 @@ async def check_invoice_payment( mint_url = settlement.mint_url or settings.primary_mint wallet = await get_wallet(mint_url, "sat") - mint_status = await run_mint_operation( - lambda: wallet.get_mint_quote(settlement.payment_hash), - op_name="get_mint_quote", - mint_url=mint_url, - ) + try: + mint_status = await run_mint_operation( + lambda: wallet.get_mint_quote(settlement.payment_hash), + op_name="get_mint_quote", + mint_url=mint_url, + ) + except Exception as error: + if not _is_quote_not_found(error): + raise + logger.info( + "Invoice quote no longer exists at mint, marking expired", + extra={"invoice_id": invoice.id, "error": str(error)}, + ) + return True if not mint_status.paid: return getattr(mint_status, "state", None) == MintQuoteState.unpaid payment_confirmed = True @@ -571,20 +580,17 @@ async def check_invoice_payment( pass if not isinstance(error, Exception): raise - if _is_quote_not_found(error): - logger.info( - f"Invoice quote no longer exists at mint, marking expired", - extra={"invoice_id": invoice.id, "error": str(error)}, - ) - return True logger.error(f"Failed to check invoice payment: {error}") return False def _is_quote_not_found(error: BaseException) -> bool: """Check if the error indicates the mint no longer has this quote.""" - message = str(error).lower() - return "quote not found" in message and ("code: 0" in message or "code 0" in message) + message = str(error) + return bool( + re.search(r"\bquote\s+not\s+found\b", message, re.IGNORECASE) + and re.search(r"\bcode\s*:?\s*0\b", message, re.IGNORECASE) + ) def _is_outputs_already_signed(error: BaseException) -> bool: diff --git a/tests/unit/test_lightning_settlement.py b/tests/unit/test_lightning_settlement.py index 602c1d97..958a5e19 100644 --- a/tests/unit/test_lightning_settlement.py +++ b/tests/unit/test_lightning_settlement.py @@ -170,15 +170,21 @@ async def test_quote_not_found_is_definitively_unpaid() -> None: @pytest.mark.asyncio -async def test_quote_not_found_without_code_0_is_not_definitively_unpaid() -> None: +@pytest.mark.parametrize( + "message", + [ + "Mint Error: quote not found (Code: 10000)", + "Mint Error: quote not found (Code: 01)", + "Mint Error: quote not found (Code: 0x10)", + ], +) +async def test_quote_not_found_without_exact_code_0_is_not_definitively_unpaid( + message: str, +) -> None: _invoice_settlement_locks.clear() invoice = _invoice(status="pending", expires_at=0) session = AsyncMock() - wallet = Mock( - get_mint_quote=AsyncMock( - side_effect=Exception("Mint Error: quote not found (Code: 10000)") - ) - ) + wallet = Mock(get_mint_quote=AsyncMock(side_effect=Exception(message))) with ( patch("routstr.lightning.get_wallet", AsyncMock(return_value=wallet)), @@ -254,6 +260,36 @@ async def test_ambiguous_invoice_mint_timeout_remains_recoverable() -> None: session.commit.assert_awaited_once() +@pytest.mark.asyncio +async def test_quote_not_found_after_payment_confirmation_is_not_unpaid() -> None: + _invoice_settlement_locks.clear() + invoice = _invoice() + session = AsyncMock() + wallet = Mock(get_mint_quote=AsyncMock(return_value=Mock(paid=True))) + state_session = AsyncMock() + state_session.exec.return_value.rowcount = 1 + + @asynccontextmanager + async def owned_session() -> AsyncIterator[AsyncMock]: + yield state_session + + with ( + patch("routstr.lightning.get_wallet", AsyncMock(return_value=wallet)), + patch("routstr.lightning.create_session", owned_session), + patch( + "routstr.lightning._mint_invoice_quote", + AsyncMock( + side_effect=Exception("Mint Error: quote not found (Code: 0)") + ), + ), + patch("routstr.lightning._reload_invoice_view", AsyncMock()), + ): + result = await check_invoice_payment(invoice, session) # type: ignore[arg-type] + + assert result is False + assert invoice.status == "settlement_pending" + + @pytest.mark.asyncio async def test_quote_lookup_timeout_is_not_definitively_unpaid() -> None: _invoice_settlement_locks.clear()