diff --git a/routstr/lightning.py b/routstr/lightning.py index a6dd7ec3..6e045925 100644 --- a/routstr/lightning.py +++ b/routstr/lightning.py @@ -601,8 +601,20 @@ async def check_invoice_payment( def _is_quote_not_found(error: BaseException) -> bool: - """Check if the error indicates the mint no longer has this quote.""" + """Check if the error indicates the mint no longer has this quote. + + Recognised variants: + - ``Mint Error: quote not found (Code: 0)`` (legacy wording) — treated as + definitive only with the exact ``0`` code, because some mints reuse the + ``quote not found`` wording for ambiguous states. + - ``Mint Error: Unknown quote (Code: 50000)`` — reported by some mint + implementations when the quote id was never issued or was dropped. The + wording itself is definitive (the mint has no record of the quote), and + the numeric code varies (seen as ``50000``), so no code check is applied. + """ message = str(error) + if re.search(r"\bunknown\s+quote\b", message, re.IGNORECASE): + return True 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) diff --git a/tests/unit/test_lightning_settlement.py b/tests/unit/test_lightning_settlement.py index f791a2d8..70f4b31a 100644 --- a/tests/unit/test_lightning_settlement.py +++ b/tests/unit/test_lightning_settlement.py @@ -217,6 +217,58 @@ async def test_quote_not_found_case_insensitive() -> None: assert result is True +@pytest.mark.asyncio +async def test_unknown_quote_is_definitively_unpaid() -> None: + """Some mints report a dropped quote as 'Unknown quote (Code: 50000)'.""" + _invoice_settlement_locks.clear() + invoice = _invoice(status="pending", expires_at=0) + session = AsyncMock() + wallet = Mock( + get_mint_quote=AsyncMock( + side_effect=Exception("Mint Error: Unknown quote (Code: 50000)") + ) + ) + + with ( + patch("routstr.lightning.get_wallet", AsyncMock(return_value=wallet)), + patch("routstr.lightning._reload_invoice_view", AsyncMock()), + ): + result = await check_invoice_payment(invoice, session) # type: ignore[arg-type] + + assert result is True + + +@pytest.mark.parametrize( + "message", + [ + # legacy wording must still require the exact code 0 + "Mint Error: quote not found (Code: 10000)", + "Mint Error: quote not found (Code: 50000)", + # not actually referring to a mint quote id + "Mint Error: unknown request", + "connection error: quote endpoint unreachable", + ], +) +def test_unknown_quote_only_matches_real_quote_missing_errors(message: str) -> None: + from routstr.lightning import _is_quote_not_found + + assert not _is_quote_not_found(Exception(message)) + + +@pytest.mark.parametrize( + "message", + [ + "Mint Error: Unknown quote (Code: 50000)", + "MINT ERROR: UNKNOWN QUOTE (CODE 50000)", + "Mint Error: unknown quote (Code: 11000)", + ], +) +def test_unknown_quote_wording_is_recognized(message: str) -> None: + from routstr.lightning import _is_quote_not_found + + assert _is_quote_not_found(Exception(message)) + + @pytest.mark.asyncio async def test_credited_invoice_is_not_minted() -> None: _invoice_settlement_locks.clear()