diff --git a/cashu/core/settings.py b/cashu/core/settings.py index ab18910b..8f305a23 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -140,7 +140,9 @@ class FakeWalletSettings(MintSettings): fakewallet_delay_incoming_payment: Optional[float] = Field(default=3.0) fakewallet_stochastic_invoice: bool = Field(default=False) fakewallet_payment_state: Optional[str] = Field(default="SETTLED") + fakewallet_payment_state_exception: Optional[bool] = Field(default=False) fakewallet_pay_invoice_state: Optional[str] = Field(default="SETTLED") + fakewallet_pay_invoice_state_exception: Optional[bool] = Field(default=False) class MintInformation(CashuSettings): diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 7e8c6aca..59073fd1 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -147,6 +147,9 @@ async def create_invoice( ) async def pay_invoice(self, quote: MeltQuote, fee_limit: int) -> PaymentResponse: + if settings.fakewallet_pay_invoice_state_exception: + raise Exception("FakeWallet pay_invoice exception") + invoice = decode(quote.request) if settings.fakewallet_delay_outgoing_payment: @@ -189,6 +192,8 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: ) async def get_payment_status(self, checking_id: str) -> PaymentStatus: + if settings.fakewallet_payment_state_exception: + raise Exception("FakeWallet get_payment_status exception") if settings.fakewallet_payment_state: return PaymentStatus( result=PaymentResult[settings.fakewallet_payment_state] diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index a8fea7dc..c08531e2 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -943,7 +943,7 @@ async def melt( melt_quote, melt_quote.fee_reserve * 1000 ) logger.debug( - f"Melt – Result: {payment.result}: preimage: {payment.preimage}," + f"Melt – Result: {payment.result.name}: preimage: {payment.preimage}," f" fee: {payment.fee.str() if payment.fee is not None else 'None'}" ) if ( @@ -967,7 +967,7 @@ async def melt( # explicitly check payment status for failed or unknown payment states checking_id = payment.checking_id or melt_quote.checking_id logger.debug( - f"Payment state is {payment.result}. Checking status for {checking_id}" + f"Payment state is {payment.result.name}.{' Error: ' + payment.error_message + '.' if payment.error_message else ''} Checking status for {checking_id}." ) try: status = await self.backends[method][unit].get_payment_status( @@ -988,13 +988,17 @@ async def melt( await self.db_write._unset_melt_quote_pending( quote=melt_quote, state=previous_state ) + if status.error_message: + logger.error( + f"Status check error: {status.error_message}" + ) raise LightningError( - f"Lightning payment failed: {payment.error_message}. Error: {status.error_message}" + f"Lightning payment failed{': ' + payment.error_message if payment.error_message else ''}." ) case _: - # Status check returned different result than payment. Something must be wrong with our implementation or the backend. Keep transaction pending and return. + # Something went wrong with our implementation or the backend. Status check returned different result than payment. Keep transaction pending and return. logger.error( - f"Payment state is {status.result} and payment was {payment.result}. Proofs for melt quote {melt_quote.quote} are stuck as PENDING. Disabling melt. Fix your Lightning backend and restart the mint." + f"Payment state is {status.result.name} and payment was {payment.result}. Proofs for melt quote {melt_quote.quote} are stuck as PENDING. Disabling melt. Fix your Lightning backend and restart the mint." ) self.disable_melt = True return PostMeltQuoteResponse.from_melt_quote(melt_quote) @@ -1013,7 +1017,9 @@ async def melt( # NOTE: This is the only branch for a successful payment case PaymentResult.PENDING | _: - logger.debug(f"Lightning payment is pending: {payment.checking_id}") + logger.debug( + f"Lightning payment is {payment.result.name}: {payment.checking_id}" + ) return PostMeltQuoteResponse.from_melt_quote(melt_quote) # melt was successful (either internal or via backend), invalidate proofs diff --git a/tests/test_mint_melt.py b/tests/test_mint_melt.py index 1a99f205..8d2d2489 100644 --- a/tests/test_mint_melt.py +++ b/tests/test_mint_melt.py @@ -295,3 +295,39 @@ async def test_melt_lightning_pay_invoice_failed_pending( # expect that proofs are pending states = await ledger.db_read.get_proofs_states([p.Y for p in wallet.proofs]) assert all([s.pending for s in states]) + + +@pytest.mark.asyncio +@pytest.mark.skipif(is_regtest, reason="only fake wallet") +async def test_melt_lightning_pay_invoice_exception_exception( + ledger: Ledger, wallet: Wallet +): + """Simulates the case where pay_invoice and get_payment_status raise an exception (due to network issues for example).""" + invoice = await wallet.request_mint(64) + await ledger.get_mint_quote(invoice.id) # fakewallet: set the quote to paid + await wallet.mint(64, id=invoice.id) + # invoice_64_sat = "lnbcrt640n1pn0r3tfpp5e30xac756gvd26cn3tgsh8ug6ct555zrvl7vsnma5cwp4g7auq5qdqqcqzzsxqyz5vqsp5xfhtzg0y3mekv6nsdnj43c346smh036t4f8gcfa2zwpxzwcryqvs9qxpqysgqw5juev8y3zxpdu0mvdrced5c6a852f9x7uh57g6fgjgcg5muqzd5474d7xgh770frazel67eejfwelnyr507q46hxqehala880rhlqspw07ta0" + invoice_62_sat = "lnbcrt620n1pn0r3vepp5zljn7g09fsyeahl4rnhuy0xax2puhua5r3gspt7ttlfrley6valqdqqcqzzsxqyz5vqsp577h763sel3q06tfnfe75kvwn5pxn344sd5vnays65f9wfgx4fpzq9qxpqysgqg3re9afz9rwwalytec04pdhf9mvh3e2k4r877tw7dr4g0fvzf9sny5nlfggdy6nduy2dytn06w50ls34qfldgsj37x0ymxam0a687mspp0ytr8" + quote_id = ( + await ledger.melt_quote( + PostMeltQuoteRequest(unit="sat", request=invoice_62_sat) + ) + ).quote + # quote = await ledger.get_melt_quote(quote_id) + settings.fakewallet_payment_state_exception = True + settings.fakewallet_pay_invoice_state_exception = True + + # we expect a pending melt quote because something has gone wrong (for example has lost connection to backend) + resp = await ledger.melt(proofs=wallet.proofs, quote=quote_id) + assert resp.state == MeltQuoteState.pending.value + + # the mint should be locked now and not allow any other melts until it is restarted + quote_id = ( + await ledger.melt_quote( + PostMeltQuoteRequest(unit="sat", request=invoice_62_sat) + ) + ).quote + await assert_err( + ledger.melt(proofs=wallet.proofs, quote=quote_id), + "Melt is disabled. Please contact the operator.", + )