diff --git a/lnbits/wallets/alby.py b/lnbits/wallets/alby.py index bc7822c0b..53d6e4fb7 100644 --- a/lnbits/wallets/alby.py +++ b/lnbits/wallets/alby.py @@ -4,6 +4,8 @@ import json from collections.abc import AsyncGenerator import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from lnbits.helpers import normalize_endpoint @@ -124,6 +126,11 @@ class AlbyWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + try: # https://api.getalby.com/payments/bolt11 r = await self.client.post( @@ -136,9 +143,12 @@ class AlbyWallet(Wallet): if r.is_error: error_message = data["message"] if "message" in data else r.text - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) - checking_id = data["payment_hash"] + data["payment_hash"] # todo: confirm with bitkarrot that having the minus is fine # other funding sources return a positive fee value fee_msat = -data["fee"] @@ -149,18 +159,21 @@ class AlbyWallet(Wallet): except KeyError as exc: logger.warning(exc) return PaymentResponse( - error_message="Server error: 'missing required fields'" + checking_id=checking_id, + error_message="Server error: 'missing required fields'", ) except json.JSONDecodeError as exc: logger.warning(exc) return PaymentResponse( - error_message="Server error: 'invalid json response'" + checking_id=checking_id, + error_message="Server error: 'invalid json response'", ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) return PaymentResponse( - error_message=f"Unable to connect to {self.endpoint}." + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: diff --git a/lnbits/wallets/blink.py b/lnbits/wallets/blink.py index edc7d1c16..b1d6c1073 100644 --- a/lnbits/wallets/blink.py +++ b/lnbits/wallets/blink.py @@ -167,6 +167,10 @@ class BlinkWallet(Wallet): async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: # https://dev.blink.sv/api/btc-ln-send # Future: add check fee estimate is < fee_limit_msat before paying invoice + try: + checking_id = bolt11_lib.decode(bolt11).payment_hash + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) payment_variables = { "input": { @@ -178,29 +182,38 @@ class BlinkWallet(Wallet): data = {"query": q.payment_query, "variables": payment_variables} try: response = await self._graphql_query(data) - - errors = ( - response.get("data", {}) - .get("lnInvoicePaymentSend", {}) - .get("errors", {}) - ) - if len(errors) > 0: - error_message = errors[0].get("message") - return PaymentResponse(ok=False, error_message=error_message) - - checking_id = bolt11_lib.decode(bolt11).payment_hash + payment_data = response.get("data", {}).get("lnInvoicePaymentSend", {}) + errors = payment_data.get("errors") or [] + status = payment_data.get("status") + if status == "FAILURE": + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=errors[0].get("message") if errors else None, + ) payment_status = await self.get_payment_status(checking_id) fee_msat = payment_status.fee_msat preimage = payment_status.preimage + if status == "SUCCESS" or payment_status.success: + ok = True + elif payment_status.failed: + ok = False + else: + ok = None return PaymentResponse( - ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage + ok=ok, + checking_id=checking_id, + fee_msat=fee_msat, + preimage=preimage, + error_message=errors[0].get("message") if errors else None, ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) return PaymentResponse( - error_message=f"Unable to connect to {self.endpoint}." + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: diff --git a/lnbits/wallets/boltz.py b/lnbits/wallets/boltz.py index c6473f181..1ad8b6d2b 100644 --- a/lnbits/wallets/boltz.py +++ b/lnbits/wallets/boltz.py @@ -123,8 +123,14 @@ class BoltzWallet(Wallet): fee_msat=fee_msat, ) - async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: - + async def pay_invoice( # noqa: C901 + self, bolt11: str, fee_limit_msat: int + ) -> PaymentResponse: + try: + invoice = decode(bolt11) + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + checking_id = invoice.payment_hash pair = boltzrpc_pb2.Pair(**{"from": boltzrpc_pb2.LBTC}) try: pair_info: boltzrpc_pb2.PairInfo @@ -132,18 +138,32 @@ class BoltzWallet(Wallet): type=boltzrpc_pb2.SUBMARINE, pair=pair ) pair_info = await self.rpc.GetPairInfo(pair_request, metadata=self.metadata) - invoice = decode(bolt11) + except AioRpcError as exc: + logger.warning(exc) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=exc.details(), + ) - if not invoice.amount_msat: - raise ValueError("amountless invoice") + if not invoice.amount_msat: + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message="amountless invoice", + ) - service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100 - estimate = int(service_fee + pair_info.fees.miner_fees * 1000) - if estimate > fee_limit_msat: - error = f"fee of {estimate} msat exceeds limit of {fee_limit_msat} msat" - - return PaymentResponse(ok=False, error_message=error) + service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100 + estimate = int(service_fee + pair_info.fees.miner_fees * 1000) + if estimate > fee_limit_msat: + error = f"fee of {estimate} msat exceeds limit of {fee_limit_msat} msat" + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=error, + ) + try: request = boltzrpc_pb2.CreateSwapRequest( invoice=bolt11, pair=pair, @@ -163,10 +183,13 @@ class BoltzWallet(Wallet): logger.warning( "Boltz invoice paid directly on liquid network using magic routing" ) - return PaymentResponse(ok=True, checking_id=invoice.payment_hash) + return PaymentResponse(ok=True, checking_id=checking_id) except AioRpcError as exc: logger.warning(exc) - return PaymentResponse(ok=False, error_message=exc.details()) + return PaymentResponse( + checking_id=checking_id, + error_message=exc.details(), + ) try: info_request = boltzrpc_pb2.GetSwapInfoRequest(id=response.id) @@ -182,18 +205,26 @@ class BoltzWallet(Wallet): ) return PaymentResponse( ok=True, - checking_id=invoice.payment_hash, + checking_id=checking_id, fee_msat=fee_msat, preimage=info.swap.preimage, ) elif info.swap.error != "": - return PaymentResponse(ok=False, error_message=info.swap.error) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=info.swap.error, + ) return PaymentResponse( - ok=False, error_message="stream stopped unexpectedly" + checking_id=checking_id, + error_message="stream stopped unexpectedly", ) except AioRpcError as exc: logger.warning(exc) - return PaymentResponse(ok=False, error_message=exc.details()) + return PaymentResponse( + checking_id=checking_id, + error_message=exc.details(), + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: diff --git a/lnbits/wallets/breez.py b/lnbits/wallets/breez.py index e32cb9b90..e32fda88c 100644 --- a/lnbits/wallets/breez.py +++ b/lnbits/wallets/breez.py @@ -238,16 +238,29 @@ else: self.sdk_services.report_issue(payment_error) # type: ignore[arg-type] except Exception as ex: logger.info(ex) - return PaymentResponse(error_message=f"exception while payment {exc!s}") - - if payment.status != BreezPaymentStatus.COMPLETE: - return PaymentResponse(ok=None, error_message="payment is pending") + return PaymentResponse( + checking_id=invoice.payment_hash, + error_message=f"exception while payment {exc!s}", + ) # let's use the payment_hash as the checking_id checking_id = invoice.payment_hash + if payment.status == BreezPaymentStatus.FAILED: + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message="payment failed", + ) + if payment.status != BreezPaymentStatus.COMPLETE: + return PaymentResponse( + ok=None, + checking_id=checking_id, + error_message="payment is pending", + ) if not isinstance(payment.details, PaymentDetails.LN): return PaymentResponse( + checking_id=checking_id, error_message="Breez SDK returned a non-LN payment details object", ) diff --git a/lnbits/wallets/breez_liquid.py b/lnbits/wallets/breez_liquid.py index c2a649a90..fcc9b9bd6 100644 --- a/lnbits/wallets/breez_liquid.py +++ b/lnbits/wallets/breez_liquid.py @@ -173,7 +173,11 @@ else: async def pay_invoice( self, bolt11: str, fee_limit_msat: int ) -> PaymentResponse: - invoice_data = bolt11_decode(bolt11) + try: + invoice_data = bolt11_decode(bolt11) + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + checking_id = invoice_data.payment_hash try: prepare_req = PrepareSendRequest(destination=bolt11) @@ -186,23 +190,33 @@ else: if req.fees_sat and req.fees_sat > fee_limit_sat: return PaymentResponse( ok=False, + checking_id=checking_id, error_message=( f"fee of {req.fees_sat} sat exceeds limit of " f"{fee_limit_sat} sat" ), ) + except Exception as exc: + logger.warning(exc) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=f"Exception while preparing payment: {exc}", + ) + try: send_response = self.sdk_services.send_payment( SendPaymentRequest(prepare_response=req) ) - except Exception as exc: logger.warning(exc) - return PaymentResponse(error_message=f"Exception while payment: {exc}") + return PaymentResponse( + checking_id=checking_id, + error_message=f"Exception while payment: {exc}", + ) payment: Payment = send_response.payment logger.debug(f"pay invoice res: {payment}") - checking_id = invoice_data.payment_hash fees = req.fees_sat * 1000 if req.fees_sat and req.fees_sat > 0 else 0 @@ -211,7 +225,8 @@ else: if not isinstance(payment.details, PaymentDetails.LIGHTNING): return PaymentResponse( - error_message="lightning payment details are not available" + checking_id=checking_id, + error_message="lightning payment details are not available", ) return PaymentResponse( diff --git a/lnbits/wallets/cliche.py b/lnbits/wallets/cliche.py index a0ef0e3ce..8beeae4e3 100644 --- a/lnbits/wallets/cliche.py +++ b/lnbits/wallets/cliche.py @@ -3,6 +3,8 @@ import hashlib import json from collections.abc import AsyncGenerator +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from websocket import create_connection @@ -104,37 +106,37 @@ class ClicheWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + ws = create_connection(self.endpoint) ws.send(f"pay-invoice --invoice {bolt11}") - checking_id, fee_msat, preimage, payment_ok = ( - None, - None, - None, - None, - ) + fee_msat, preimage, payment_ok = None, None, None for _ in range(2): r = ws.recv() data = json.loads(r) - checking_id, fee_msat, preimage, payment_ok = ( - None, - None, - None, - None, - ) if data.get("error") is not None: error_message = data["error"].get("message") - return PaymentResponse(ok=False, error_message=error_message) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=error_message, + ) if data.get("method") == "payment_succeeded": payment_ok = True - checking_id = data["params"]["payment_hash"] fee_msat = data["params"]["fee_msatoshi"] preimage = data["params"]["preimage"] continue if data.get("result") is None: - return PaymentResponse(error_message="result is None") + return PaymentResponse( + checking_id=checking_id, + error_message="result is None", + ) return PaymentResponse( ok=payment_ok, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage diff --git a/lnbits/wallets/clnrest.py b/lnbits/wallets/clnrest.py index 8d34cef34..73a757f42 100644 --- a/lnbits/wallets/clnrest.py +++ b/lnbits/wallets/clnrest.py @@ -255,15 +255,19 @@ class CLNRestWallet(Wallet): invoice = decode(bolt11) except Bolt11Exception as exc: return PaymentResponse(ok=False, error_message=str(exc)) + checking_id = invoice.payment_hash if not invoice.amount_msat or invoice.amount_msat <= 0: return PaymentResponse( - ok=False, error_message="0 amount invoices are not allowed" + ok=False, + checking_id=checking_id, + error_message="0 amount invoices are not allowed", ) if not settings.clnrest_pay_rune and not settings.clnrest_renepay_rune: return PaymentResponse( ok=False, + checking_id=checking_id, error_message="Unable to pay invoice without a pay or renepay rune", ) @@ -296,11 +300,16 @@ class CLNRestWallet(Wallet): if "payment_preimage" not in data: error_message = data.get("error", "No payment preimage in response") logger.warning(error_message) - return PaymentResponse(error_message=error_message) + failed = self.statuses.get(data.get("status")) is False + return PaymentResponse( + ok=False if failed else None, + checking_id=checking_id, + error_message=error_message, + ) return PaymentResponse( ok=self.statuses.get(data["status"]), - checking_id=data["payment_hash"], + checking_id=checking_id, fee_msat=data["amount_sent_msat"] - data["amount_msat"], preimage=data["payment_preimage"], ) @@ -311,18 +320,31 @@ class CLNRestWallet(Wallet): error_code = int(error.get("code", 0)) error_message = error.get("message", "Unknown error") if error_code in self.pay_failure_error_codes: - return PaymentResponse(ok=False, error_message=error_message) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=error_message, + ) else: - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except Exception: error_message = f"Error parsing response from {self.url}: {exc!s}" logger.warning(error_message) - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) error_message = f"Unable to connect to {self.url}." - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: data: dict = {"payment_hash": checking_id} diff --git a/lnbits/wallets/corelightning.py b/lnbits/wallets/corelightning.py index e21cb4f13..e68e6d038 100644 --- a/lnbits/wallets/corelightning.py +++ b/lnbits/wallets/corelightning.py @@ -152,15 +152,14 @@ class CoreLightningWallet(Wallet): invoice = bolt11_decode(bolt11) except Bolt11Exception as exc: return PaymentResponse(ok=False, error_message=str(exc)) + checking_id = invoice.payment_hash try: - previous_payment = await self.get_payment_status(invoice.payment_hash) - if previous_payment.paid: - return PaymentResponse(ok=False, error_message="invoice already paid") - if not invoice.amount_msat or invoice.amount_msat <= 0: return PaymentResponse( - ok=False, error_message="CLN 0 amount invoice not supported" + ok=False, + checking_id=checking_id, + error_message="CLN 0 amount invoice not supported", ) # maxfee overrides both maxfeepercent and exemptfee defaults (and @@ -178,7 +177,7 @@ class CoreLightningWallet(Wallet): fee_msat = -int(r["amount_sent_msat"] - r["amount_msat"]) return PaymentResponse( - True, r["payment_hash"], fee_msat, r["payment_preimage"], None + True, checking_id, fee_msat, r["payment_preimage"], None ) except RpcError as exc: logger.warning(exc) @@ -187,23 +186,35 @@ class CoreLightningWallet(Wallet): if error_code in self.pay_failure_error_codes: error_message = exc.error.get("message", error_code) # type: ignore return PaymentResponse( - ok=False, error_message=f"Payment failed: {error_message}" + ok=False, + checking_id=checking_id, + error_message=f"Payment failed: {error_message}", ) else: error_message = f"Payment failed: {exc.error}" - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except Exception: error_message = f"RPC '{exc.method}' failed with '{exc.error}'." - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except KeyError as exc: logger.warning(exc) return PaymentResponse( - error_message="Server error: 'missing required fields'" + checking_id=checking_id, + error_message="Server error: 'missing required fields'", ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) - return PaymentResponse(error_message=f"Payment failed: '{exc}'.") + return PaymentResponse( + checking_id=checking_id, + error_message=f"Payment failed: '{exc}'.", + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: @@ -239,7 +250,7 @@ class CoreLightningWallet(Wallet): return PaymentPendingStatus() if not r["pays"]: # no payment with this payment_hash is found - return PaymentFailedStatus() + return PaymentPendingStatus() payment_resp = r["pays"][-1] diff --git a/lnbits/wallets/corelightningrest.py b/lnbits/wallets/corelightningrest.py index 79fd1cf02..d7ed1abf3 100644 --- a/lnbits/wallets/corelightningrest.py +++ b/lnbits/wallets/corelightningrest.py @@ -181,10 +181,15 @@ class CoreLightningRestWallet(Wallet): invoice = decode(bolt11) except Bolt11Exception as exc: return PaymentResponse(ok=False, error_message=str(exc)) + checking_id = invoice.payment_hash if not invoice.amount_msat or invoice.amount_msat <= 0: error_message = "0 amount invoices are not allowed" - return PaymentResponse(ok=False, error_message=error_message) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=error_message, + ) try: r = await self.client.post( f"{self.url}/v1/pay", @@ -201,10 +206,12 @@ class CoreLightningRestWallet(Wallet): status = self.statuses.get(data["status"]) if "payment_preimage" not in data: return PaymentResponse( - ok=status, error_message=data.get("error") or "unknown error" + ok=False if status is False else None, + checking_id=checking_id, + error_message=data.get("error") or "unknown error", ) - checking_id = data["payment_hash"] + data["payment_hash"] preimage = data["payment_preimage"] fee_msat = data["msatoshi_sent"] - data["msatoshi"] @@ -218,26 +225,41 @@ class CoreLightningRestWallet(Wallet): error_code = int(data["error"]["code"]) if error_code in self.pay_failure_error_codes: error_message = data["error"]["message"] - return PaymentResponse(ok=False, error_message=error_message) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=error_message, + ) error_message = f"REST failed with {data['error']['message']}." - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except Exception as exc: error_message = f"Unable to connect to {self.url}." - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except json.JSONDecodeError: return PaymentResponse( - error_message="Server error: 'invalid json response'" + checking_id=checking_id, + error_message="Server error: 'invalid json response'", ) except KeyError as exc: logger.warning(exc) return PaymentResponse( - error_message="Server error: 'missing required fields'" + checking_id=checking_id, + error_message="Server error: 'missing required fields'", ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) - return PaymentResponse(error_message=f"Unable to connect to {self.url}.") + return PaymentResponse( + checking_id=checking_id, + error_message=f"Unable to connect to {self.url}.", + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get( diff --git a/lnbits/wallets/eclair.py b/lnbits/wallets/eclair.py index dc5ed590d..2ae9ff273 100644 --- a/lnbits/wallets/eclair.py +++ b/lnbits/wallets/eclair.py @@ -8,6 +8,8 @@ from decimal import Decimal from typing import Any import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from websockets import connect @@ -145,6 +147,11 @@ class EclairWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + try: r = await self.client.post( "/payinvoice", @@ -155,28 +162,39 @@ class EclairWallet(Wallet): data = r.json() if "error" in data: - return PaymentResponse(error_message=data["error"]) + return PaymentResponse( + checking_id=checking_id, error_message=data["error"] + ) if r.is_error: - return PaymentResponse(error_message=r.text) + return PaymentResponse(checking_id=checking_id, error_message=r.text) if data["type"] == "payment-failed": - return PaymentResponse(ok=False, error_message="payment failed") + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message="payment failed", + ) - checking_id = data["paymentHash"] + data["paymentHash"] preimage = data["paymentPreimage"] except json.JSONDecodeError: return PaymentResponse( - error_message="Server error: 'invalid json response'" + checking_id=checking_id, + error_message="Server error: 'invalid json response'", ) except KeyError: return PaymentResponse( - error_message="Server error: 'missing required fields'" + checking_id=checking_id, + error_message="Server error: 'missing required fields'", ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) - return PaymentResponse(error_message=f"Unable to connect to {self.url}.") + return PaymentResponse( + checking_id=checking_id, + error_message=f"Unable to connect to {self.url}.", + ) payment_status: PaymentStatus = await self.get_payment_status(checking_id) success = True if payment_status.success else None diff --git a/lnbits/wallets/lnbits.py b/lnbits/wallets/lnbits.py index c14a41706..0e7b6bfd9 100644 --- a/lnbits/wallets/lnbits.py +++ b/lnbits/wallets/lnbits.py @@ -3,6 +3,8 @@ import json from collections.abc import AsyncGenerator import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from websockets import connect @@ -118,6 +120,11 @@ class LNbitsWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + try: r = await self.client.post( url="/api/v1/payments", @@ -128,7 +135,7 @@ class LNbitsWallet(Wallet): r.raise_for_status() data = r.json() - checking_id = data["payment_hash"] + data["payment_hash"] # we do this to get the fee and preimage payment: PaymentStatus = await self.get_payment_status(checking_id) @@ -147,25 +154,38 @@ class LNbitsWallet(Wallet): data = exc.response.json() error_message = f"Payment {data['status']}: {data['detail']}." if data["status"] == "failed": - return PaymentResponse(ok=False, error_message=error_message) - return PaymentResponse(error_message=error_message) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=error_message, + ) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except Exception as exc: error_message = f"Unable to connect to {self.endpoint}." - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) except json.JSONDecodeError: return PaymentResponse( - error_message="Server error: 'invalid json response'" + checking_id=checking_id, + error_message="Server error: 'invalid json response'", ) except KeyError: return PaymentResponse( - error_message="Server error: 'missing required fields'" + checking_id=checking_id, + error_message="Server error: 'missing required fields'", ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) return PaymentResponse( - error_message=f"Unable to connect to {self.endpoint}." + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py index 089208e61..c81d3cff7 100644 --- a/lnbits/wallets/lndgrpc.py +++ b/lnbits/wallets/lndgrpc.py @@ -5,6 +5,8 @@ from hashlib import sha256 from os import environ import grpc +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from lnbits.helpers import normalize_endpoint @@ -186,6 +188,11 @@ class LndWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + # fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000) req = SendPaymentRequest( payment_request=bolt11, @@ -200,30 +207,32 @@ class LndWallet(Wallet): res: Payment = await self.router_rpc.SendPaymentV2(req).read() except Exception as exc: logger.warning(exc) - return PaymentResponse(error_message=str(exc)) + return PaymentResponse(checking_id=checking_id, error_message=str(exc)) if res.status == Payment.PaymentStatus.SUCCEEDED: return PaymentResponse( ok=True, - checking_id=res.payment_hash, + checking_id=checking_id, fee_msat=abs(res.fee_msat), preimage=res.payment_preimage, ) elif res.status == Payment.PaymentStatus.FAILED: error_message = PaymentFailureReason.Name(res.failure_reason) return PaymentResponse( - ok=False, error_message=f"Payment failed: {error_message}" + ok=False, + checking_id=checking_id, + error_message=f"Payment failed: {error_message}", ) elif res.status == Payment.PaymentStatus.IN_FLIGHT: return PaymentResponse( ok=None, - checking_id=res.payment_hash, + checking_id=checking_id, error_message="Payment is IN_FLIGHT.", ) else: return PaymentResponse( ok=None, - checking_id=res.payment_hash, + checking_id=checking_id, error_message="Payment is non-existant.", ) diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 0848a7ad9..735f76151 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -6,6 +6,8 @@ from collections.abc import AsyncGenerator from typing import Any import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from lnbits.helpers import normalize_endpoint @@ -145,6 +147,11 @@ class LndRestWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + req = { "payment_request": bolt11, "fee_limit_msat": fee_limit_msat, @@ -164,29 +171,36 @@ class LndRestWallet(Wallet): data = r.json() except json.JSONDecodeError: return PaymentResponse( - error_message="Server error: 'invalid json response'" + checking_id=checking_id, + error_message="Server error: 'invalid json response'", ) except Exception as exc: logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.") return PaymentResponse( - error_message=f"Unable to connect to {self.endpoint}." + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", ) payment_error = data.get("payment_error") if payment_error: logger.warning(f"LndRestWallet payment_error: {payment_error}.") - return PaymentResponse(ok=False, error_message=payment_error) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=payment_error, + ) try: payment = data["result"] status = payment["status"] - checking_id = payment["payment_hash"] + payment["payment_hash"] preimage = payment["payment_preimage"] fee_msat = abs(int(payment["fee_msat"])) except KeyError as exc: logger.warning(exc) return PaymentResponse( - error_message="Server error: 'missing required fields'" + checking_id=checking_id, + error_message="Server error: 'missing required fields'", ) if status == "SUCCEEDED": @@ -201,7 +215,7 @@ class LndRestWallet(Wallet): elif status == "IN_FLIGHT": return PaymentResponse(ok=None, checking_id=checking_id) return PaymentResponse( - ok=False, + ok=None, checking_id=checking_id, error_message="Server error: 'unknown payment status returned'", ) diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 738faf9ea..bcc074929 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -3,6 +3,8 @@ import hashlib from collections.abc import AsyncGenerator import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from lnbits.helpers import normalize_endpoint @@ -38,6 +40,7 @@ class LNPayWallet(Wallet): ) self.wallet_key = wallet_key self.endpoint = normalize_endpoint(settings.lnpay_api_endpoint) + self.payment_ids: dict[str, str] = {} headers = { "X-Api-Key": settings.lnpay_api_key, @@ -105,33 +108,61 @@ class LNPayWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: - r = await self.client.post( - f"/wallet/{self.wallet_key}/withdraw", - json={"payment_request": bolt11}, - timeout=None, - ) + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) try: + r = await self.client.post( + f"/wallet/{self.wallet_key}/withdraw", + json={"payment_request": bolt11}, + timeout=None, + ) data = r.json() - except Exception: - return PaymentResponse(ok=False, error_message="Got invalid JSON.") + except Exception as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message="Unable to determine payment status.", + ) if r.is_error: - return PaymentResponse(ok=False, error_message=data["message"]) + error_message = data.get("message", r.text) + return PaymentResponse( + ok=False if r.is_client_error else None, + checking_id=checking_id, + error_message=error_message, + ) - checking_id = data["lnTx"]["id"] - fee_msat = 0 - preimage = data["lnTx"]["payment_preimage"] + try: + payment_data = data["lnTx"] + provider_id = payment_data["id"] + except (KeyError, TypeError) as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message="Server error: 'missing required fields'", + ) + + self.payment_ids[checking_id] = provider_id + preimage = payment_data.get("payment_preimage") + if not preimage: + return PaymentResponse( + checking_id=checking_id, + error_message="Payment status is pending.", + ) return PaymentResponse( - ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage + ok=True, checking_id=checking_id, fee_msat=0, preimage=preimage ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: return await self.get_payment_status(checking_id) async def get_payment_status(self, checking_id: str) -> PaymentStatus: + provider_id = self.payment_ids.get(checking_id, checking_id) r = await self.client.get( - url=f"/lntx/{checking_id}", + url=f"/lntx/{provider_id}", ) if r.is_error: @@ -141,7 +172,10 @@ class LNPayWallet(Wallet): preimage = data["payment_preimage"] fee_msat = data["fee_msat"] statuses = {0: None, 1: True, -1: False} - return PaymentStatus(statuses[data["settled"]], fee_msat, preimage) + status = PaymentStatus(statuses[data["settled"]], fee_msat, preimage) + if status.success or status.failed: + self.payment_ids.pop(checking_id, None) + return status async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: self.queue: asyncio.Queue = asyncio.Queue(0) diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py index 5a15be1a0..15c3e9cf8 100644 --- a/lnbits/wallets/lntips.py +++ b/lnbits/wallets/lntips.py @@ -5,6 +5,8 @@ import time from collections.abc import AsyncGenerator import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from lnbits.helpers import normalize_endpoint @@ -103,26 +105,52 @@ class LnTipsWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: - r = await self.client.post( - "/api/v1/payinvoice", - json={"pay_req": bolt11}, - timeout=None, - ) + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + + try: + r = await self.client.post( + "/api/v1/payinvoice", + json={"pay_req": bolt11}, + timeout=None, + ) + except Exception as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", + ) + if r.is_error: - return PaymentResponse(ok=False, error_message=r.text) + return PaymentResponse( + ok=False if r.is_client_error else None, + checking_id=checking_id, + error_message=r.text, + ) - if "error" in r.json(): - try: - data = r.json() + try: + data = r.json() + if "error" in data: error_message = data["error"] - except Exception: - error_message = r.text - return PaymentResponse(ok=False, error_message=error_message) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=error_message, + ) + + details = data["details"] + details["payment_hash"] + fee_msat = -details["fee"] + preimage = details["preimage"] + except (json.JSONDecodeError, KeyError, TypeError) as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message="Server error: 'invalid payment response'", + ) - data = r.json()["details"] - checking_id = data["payment_hash"] - fee_msat = -data["fee"] - preimage = data["preimage"] return PaymentResponse( ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage ) diff --git a/lnbits/wallets/nwc.py b/lnbits/wallets/nwc.py index 860f38e3b..208eba73d 100644 --- a/lnbits/wallets/nwc.py +++ b/lnbits/wallets/nwc.py @@ -579,11 +579,14 @@ class NWCWallet(Wallet): return StatusResponse(str(e), 0) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + payment_hash = bolt11_decode(bolt11).payment_hash + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + try: resp = await self.conn.call("pay_invoice", {"invoice": bolt11}) preimage = resp.get("preimage", None) - invoice_data = bolt11_decode(bolt11) - payment_hash = invoice_data.payment_hash # pay_invoice doesn't return payment data, so we need # to call lookup_invoice too (if supported) await self.conn.get_info() @@ -625,20 +628,19 @@ class NWCWallet(Wallet): "QUOTA_EXCEEDED", "RESTRICTED", "UNAUTHORIZED", - "INTERNAL", - "OTHER", "PAYMENT_FAILED", ] failed = e.code in failure_codes return PaymentResponse( ok=None if not failed else False, - error_message=e.message if failed else None, + checking_id=payment_hash, + error_message=e.message, ) except Exception as e: msg = "Error paying invoice: " + str(e) logger.error(msg) # assume pending - return PaymentResponse(error_message=msg) + return PaymentResponse(checking_id=payment_hash, error_message=msg) async def _get_status_via_transactions( self, checking_id: str, unpaid_filters: list[bool] diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index f05349746..dcf836acb 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -2,6 +2,8 @@ import asyncio from collections.abc import AsyncGenerator import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from loguru import logger from lnbits.exceptions import UnsupportedError @@ -40,6 +42,7 @@ class OpenNodeWallet(Wallet): self.key = key self.endpoint = normalize_endpoint(settings.opennode_api_endpoint) + self.payment_ids: dict[str, str] = {} headers = { "Authorization": self.key, @@ -100,24 +103,66 @@ class OpenNodeWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: - r = await self.client.post( - "/v2/withdrawals", - json={"type": "ln", "address": bolt11}, - timeout=None, - ) + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + + try: + r = await self.client.post( + "/v2/withdrawals", + json={"type": "ln", "address": bolt11}, + timeout=None, + ) + data = r.json() + except Exception as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message="Unable to determine payment status.", + ) if r.is_error: - error_message = r.json()["message"] + error_message = data.get("message", r.text) logger.warning(error_message) - return PaymentResponse(ok=None, error_message=error_message) + return PaymentResponse( + ok=False if r.is_client_error else None, + checking_id=checking_id, + error_message=error_message, + ) - data = r.json()["data"] - checking_id = data["id"] - fee_msat = -data["fee"] * 1000 - # pending - if data["status"] != "paid": - return PaymentResponse(ok=None, checking_id=checking_id, fee_msat=fee_msat) - return PaymentResponse(ok=True, checking_id=checking_id, fee_msat=fee_msat) + try: + payment_data = data["data"] + provider_id = payment_data["id"] + except (KeyError, TypeError) as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message="Server error: 'missing required fields'", + ) + + self.payment_ids[checking_id] = provider_id + try: + fee_msat = -payment_data["fee"] * 1000 + status = payment_data["status"] + except (KeyError, TypeError) as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message="Server error: 'missing required fields'", + ) + + if status in {"paid", "confirmed"}: + return PaymentResponse(ok=True, checking_id=checking_id, fee_msat=fee_msat) + if status in {"error", "failed"}: + self.payment_ids.pop(checking_id, None) + return PaymentResponse( + ok=False, + checking_id=checking_id, + fee_msat=fee_msat, + error_message=payment_data.get("error") or "Payment failed.", + ) + return PaymentResponse(ok=None, checking_id=checking_id, fee_msat=fee_msat) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get(f"/v1/charge/{checking_id}") @@ -128,21 +173,29 @@ class OpenNodeWallet(Wallet): return PaymentStatus(statuses[data.get("status")]) async def get_payment_status(self, checking_id: str) -> PaymentStatus: - r = await self.client.get(f"/v1/withdrawal/{checking_id}") + provider_id = self.payment_ids.get(checking_id, checking_id) + r = await self.client.get(f"/v1/withdrawal/{provider_id}") if r.is_error: return PaymentPendingStatus() - data = r.json()["data"] - statuses = { - "initial": None, - "pending": None, - "confirmed": True, - "error": None, - "failed": False, - } - fee_msat = -data.get("fee") * 1000 - return PaymentStatus(statuses[data.get("status")], fee_msat) + try: + data = r.json()["data"] + statuses = { + "initial": None, + "pending": None, + "confirmed": True, + "error": False, + "failed": False, + } + fee_msat = -data["fee"] * 1000 + status = PaymentStatus(statuses[data["status"]], fee_msat) + if status.success or status.failed: + self.payment_ids.pop(checking_id, None) + return status + except Exception as exc: + logger.warning(exc) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: self.queue: asyncio.Queue = asyncio.Queue(0) diff --git a/lnbits/wallets/phoenixd.py b/lnbits/wallets/phoenixd.py index 0d508b2d1..0f80cd933 100644 --- a/lnbits/wallets/phoenixd.py +++ b/lnbits/wallets/phoenixd.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import Any import httpx +from bolt11 import Bolt11Exception +from bolt11 import decode as bolt11_decode from embit.bip39 import mnemonic_is_valid from httpx import RequestError, TimeoutException from loguru import logger @@ -191,6 +193,11 @@ class PhoenixdWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Bolt11Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + try: r = await self.client.post( "/payinvoice", @@ -206,17 +213,26 @@ class PhoenixdWallet(Wallet): # be safe and return pending on timeouts msg = f"Timeout connecting to {self.endpoint}. keep pending..." logger.warning(msg) - return PaymentResponse(ok=None, error_message=msg) + return PaymentResponse( + ok=None, + checking_id=checking_id, + error_message=msg, + ) except RequestError as exc: - # RequestError is raised when the request never hit the destination server msg = f"Unable to connect to {self.endpoint}." logger.warning(msg) logger.warning(exc) - return PaymentResponse(ok=False, error_message=msg) + return PaymentResponse( + ok=None, + checking_id=checking_id, + error_message=msg, + ) except Exception as exc: logger.warning(exc) return PaymentResponse( - ok=None, error_message=f"Unable to connect to {self.endpoint}." + ok=None, + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", ) try: @@ -224,9 +240,12 @@ class PhoenixdWallet(Wallet): if "routingFeeSat" not in data and ("reason" in data or "message" in data): error_message = data.get("reason", data.get("message", "Unknown error")) - return PaymentResponse(error_message=error_message) + return PaymentResponse( + checking_id=checking_id, + error_message=error_message, + ) - checking_id = data["paymentHash"] + data["paymentHash"] fee_msat = -int(data["routingFeeSat"]) * 1000 preimage = data["paymentPreimage"] return PaymentResponse( @@ -238,17 +257,20 @@ class PhoenixdWallet(Wallet): except json.JSONDecodeError: return PaymentResponse( - error_message="Server error: 'invalid json response'" + checking_id=checking_id, + error_message="Server error: 'invalid json response'", ) except KeyError: return PaymentResponse( - error_message="Server error: 'missing required fields'" + checking_id=checking_id, + error_message="Server error: 'missing required fields'", ) except Exception as exc: logger.info(f"Failed to pay invoice {bolt11}") logger.warning(exc) return PaymentResponse( - error_message=f"Unable to connect to {self.endpoint}." + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index 90dcc05e0..f786f2ceb 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -5,6 +5,7 @@ from collections.abc import AsyncGenerator from secrets import token_urlsafe import httpx +from bolt11 import decode as bolt11_decode from loguru import logger from lnbits.helpers import normalize_endpoint @@ -147,6 +148,11 @@ class SparkWallet(Wallet): return InvoiceResponse(ok=False, error_message=str(e)) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + try: r = await self.pay( bolt11=bolt11, @@ -156,7 +162,7 @@ class SparkWallet(Wallet): preimage = r["payment_preimage"] return PaymentResponse( ok=True, - checking_id=r["payment_hash"], + checking_id=checking_id, fee_msat=fee_msat, preimage=preimage, ) @@ -164,27 +170,36 @@ class SparkWallet(Wallet): except (SparkError, UnknownError) as exc: listpays = await self.listpays(bolt11) if not listpays: - return PaymentResponse(ok=False, error_message=str(exc)) + return PaymentResponse( + checking_id=checking_id, + error_message=str(exc), + ) pays = listpays["pays"] if len(pays) == 0: - return PaymentResponse(ok=False, error_message=str(exc)) + return PaymentResponse( + checking_id=checking_id, + error_message=str(exc), + ) pay = pays[0] - payment_hash = pay["payment_hash"] if len(pays) > 1: raise SparkError( - f"listpays({payment_hash}) returned an unexpected response:" + f"listpays({checking_id}) returned an unexpected response:" f" {listpays}" ) from exc if pay["status"] == "failed": - return PaymentResponse(ok=False, error_message=str(exc)) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message=str(exc), + ) if pay["status"] == "pending": - return PaymentResponse(ok=None, checking_id=payment_hash) + return PaymentResponse(ok=None, checking_id=checking_id) if pay["status"] == "complete": r = pay @@ -198,12 +213,15 @@ class SparkWallet(Wallet): preimage = r["payment_preimage"] return PaymentResponse( ok=True, - checking_id=r["payment_hash"], + checking_id=checking_id, fee_msat=fee_msat, preimage=preimage, ) else: - return PaymentResponse(ok=False, error_message=str(exc)) + return PaymentResponse( + checking_id=checking_id, + error_message=str(exc), + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: @@ -235,7 +253,7 @@ class SparkWallet(Wallet): return PaymentPendingStatus() if not r["pays"]: - return PaymentFailedStatus() + return PaymentPendingStatus() if r["pays"][0]["payment_hash"] == checking_id: status = r["pays"][0]["status"] if status == "complete": diff --git a/lnbits/wallets/sparkl2.py b/lnbits/wallets/sparkl2.py index 66c33d3e4..a94cf4c68 100644 --- a/lnbits/wallets/sparkl2.py +++ b/lnbits/wallets/sparkl2.py @@ -46,6 +46,7 @@ class SparkL2Wallet(Wallet): self._sidecar_path = Path(settings.lnbits_data_folder, "light_spark") self.pending_invoices: list[str] = [] + self.payment_ids: dict[str, str] = {} self.endpoint = "http://127.0.0.1:8765" self._api_key = uuid.uuid4().hex @@ -141,30 +142,30 @@ class SparkL2Wallet(Wallet): return InvoiceResponse(ok=False, error_message=str(e)) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + payment_hash = bolt11_decode(bolt11).payment_hash + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + try: max_fee_sats = (int(fee_limit_msat) + 999) // 1000 logger.info( f"Paying invoice via Spark sidecar with max fee {max_fee_sats} sats." ) - payment_hash = None - try: - payment_hash = bolt11_decode(bolt11).payment_hash - except Exception as exc: - logger.warning(exc) - payment_hash = None payload = { "bolt11": bolt11, "max_fee_sats": max_fee_sats, "payment_hash": payment_hash, } res = await self._request("POST", "/v1/payments", payload) - checking_id = res.get("checking_id") - if not checking_id: + provider_id = res.get("payment_hash") or res.get("checking_id") + if not provider_id: return PaymentResponse( - ok=False, + checking_id=payment_hash, error_message="Spark sidecar payment response missing checking_id.", ) + self.payment_ids[payment_hash] = provider_id status = res.get("status") fee_msat = res.get("fee_msat") ok = None @@ -172,13 +173,16 @@ class SparkL2Wallet(Wallet): ok = self._map_payment_ok(status) return PaymentResponse( ok=ok, - checking_id=checking_id, + checking_id=payment_hash, fee_msat=int(fee_msat) if fee_msat is not None else None, preimage=res.get("preimage"), ) except Exception as e: - return PaymentResponse(ok=False, error_message=str(e)) + return PaymentResponse( + checking_id=payment_hash, + error_message=str(e), + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: @@ -193,7 +197,8 @@ class SparkL2Wallet(Wallet): async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: - res = await self._request("GET", f"/v1/payments/{checking_id}") + provider_id = self.payment_ids.get(checking_id, checking_id) + res = await self._request("GET", f"/v1/payments/{provider_id}") status = res.get("status") fee_msat = res.get("fee_msat") preimage = res.get("preimage") @@ -201,11 +206,13 @@ class SparkL2Wallet(Wallet): return PaymentPendingStatus() mapped = self._map_payment_status(status) if mapped.success: + self.payment_ids.pop(checking_id, None) return PaymentSuccessStatus( fee_msat=int(fee_msat) if fee_msat is not None else None, preimage=preimage, ) if mapped.failed: + self.payment_ids.pop(checking_id, None) return PaymentFailedStatus() return PaymentPendingStatus() except Exception as exc: diff --git a/lnbits/wallets/strike.py b/lnbits/wallets/strike.py index 8fbbb7e1c..2b5d7054e 100644 --- a/lnbits/wallets/strike.py +++ b/lnbits/wallets/strike.py @@ -241,12 +241,20 @@ class StrikeWallet(Wallet): # 1) Create a payment quote quote_id, error = await self._create_payment_quote(bolt11) if error or not quote_id: - return PaymentResponse(ok=False, error_message=error or "Unknown error") + return PaymentResponse( + ok=False, + checking_id=payment_hash, + error_message=error or "Unknown error", + ) # 2) Execute the payment quote + self.pending_payments[payment_hash] = quote_id data, error = await self._execute_payment_quote(quote_id) if error or not data: - return PaymentResponse(ok=False, error_message=error or "Unknown error") + return PaymentResponse( + checking_id=payment_hash, + error_message=error or "Unknown error", + ) state = data.get("state", "").upper() payment_id = data.get("paymentId") @@ -256,6 +264,7 @@ class StrikeWallet(Wallet): # Handle successful payment if state in {"SUCCEEDED", "COMPLETED"}: + self.pending_payments.pop(payment_hash, None) preimage = self._extract_preimage(data) return PaymentResponse( ok=True, @@ -267,6 +276,7 @@ class StrikeWallet(Wallet): # Handle failed payment failed_states = {"CANCELED", "FAILED", "TIMED_OUT"} if state in failed_states: + self.pending_payments.pop(payment_hash, None) logger.warning( f"Strike payment {payment_id} failed with state: {state}" ) @@ -276,9 +286,6 @@ class StrikeWallet(Wallet): error_message=f"Payment {state.lower()}", ) - # Store mapping for later polling - self.pending_payments[payment_hash] = quote_id - # Treat all other states as pending return PaymentResponse(ok=None, checking_id=payment_hash) @@ -289,12 +296,17 @@ class StrikeWallet(Wallet): f"body: {http_exc.response.text}" ) return PaymentResponse( - ok=False, + ok=None, + checking_id=payment_hash, error_message=f"Strike API error: {http_exc.response.status_code}", ) except Exception as e: logger.warning(f"Strike payment exception: {e}", exc_info=True) - return PaymentResponse(ok=None, error_message=f"Error: {e!s}") + return PaymentResponse( + ok=None, + checking_id=payment_hash, + error_message=f"Error: {e!s}", + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: @@ -341,25 +353,14 @@ class StrikeWallet(Wallet): async def get_payment_status(self, checking_id: str) -> PaymentStatus: quote_id = self.pending_payments.get(checking_id) - + if not quote_id: + return PaymentPendingStatus() try: - # Attempt 1: Use quote_id if available (from in-memory store) - if quote_id: - status = await self._get_payment_status_by_quote_id( - checking_id, quote_id - ) - if status: - return status + status = await self._get_payment_status_by_quote_id(checking_id, quote_id) + return status or PaymentPendingStatus() except Exception as e: logger.warning(e) logger.debug(f"Error while fetching payment by quote id {checking_id}.") - - try: - # Attempt 2: Fallback - Use paymentId (checking_id) directly. - return await self._get_payment_status_by_checking_id(checking_id) - except Exception as e: - logger.warning(e) - logger.debug(f"Error while fetching payment {checking_id}.") return PaymentPendingStatus() async def get_invoices( @@ -625,7 +626,7 @@ class StrikeWallet(Wallet): if state in {"SUCCEEDED", "COMPLETED"}: self.pending_payments.pop(checking_id, None) return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) - if state == "FAILED": + if state in {"CANCELED", "FAILED", "TIMED_OUT"}: self.pending_payments.pop(checking_id, None) return PaymentFailedStatus() diff --git a/lnbits/wallets/zbd.py b/lnbits/wallets/zbd.py index 1377e0db8..437bfe584 100644 --- a/lnbits/wallets/zbd.py +++ b/lnbits/wallets/zbd.py @@ -105,27 +105,48 @@ class ZBDWallet(Wallet): async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: # https://api.zebedee.io/v0/payments - r = await self.client.post( - "payments", - json={ - "invoice": bolt11, - "description": "", - "amount": "", - "internalId": "", - "callbackUrl": "", - }, - timeout=40, - ) + try: + checking_id = bolt11_decode(bolt11).payment_hash + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + + try: + r = await self.client.post( + "payments", + json={ + "invoice": bolt11, + "description": "", + "amount": "", + "internalId": "", + "callbackUrl": "", + }, + timeout=40, + ) + data = r.json() + except Exception as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message=f"Unable to connect to {self.endpoint}.", + ) if r.is_error: - error_message = r.json()["message"] - return PaymentResponse(ok=False, error_message=error_message) + error_message = data.get("message", r.text) + return PaymentResponse( + ok=False if r.is_client_error else None, + checking_id=checking_id, + error_message=error_message, + ) - data = r.json() - - checking_id = bolt11_decode(bolt11).payment_hash - fee_msat = -int(data["data"]["fee"]) - preimage = data["data"]["preimage"] + try: + fee_msat = -int(data["data"]["fee"]) + preimage = data["data"]["preimage"] + except (KeyError, TypeError, ValueError) as exc: + logger.warning(exc) + return PaymentResponse( + checking_id=checking_id, + error_message="Server error: 'missing required fields'", + ) return PaymentResponse( ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage diff --git a/tests/wallets/fixtures/json/fixtures_rest.json b/tests/wallets/fixtures/json/fixtures_rest.json index bd49a4628..4a54ced4a 100644 --- a/tests/wallets/fixtures/json/fixtures_rest.json +++ b/tests/wallets/fixtures/json/fixtures_rest.json @@ -1322,7 +1322,7 @@ "success": false, "pending": false, "failed": true, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null }, @@ -1518,7 +1518,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null, "error_message": "Test Error" @@ -1612,7 +1612,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null }, @@ -1709,7 +1709,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null }, @@ -1827,7 +1827,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null }, @@ -1957,7 +1957,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null } diff --git a/tests/wallets/fixtures/json/fixtures_rpc.json b/tests/wallets/fixtures/json/fixtures_rpc.json index 58f729a3e..d0dd56dcf 100644 --- a/tests/wallets/fixtures/json/fixtures_rpc.json +++ b/tests/wallets/fixtures/json/fixtures_rpc.json @@ -702,7 +702,7 @@ "expect": { "error_message": null, "success": true, - "checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417", + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": 50, "preimage": "0000000000000000000000000000000000000000000000000000000000000000" }, @@ -843,7 +843,7 @@ "success": false, "pending": false, "failed": true, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null }, @@ -1024,7 +1024,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null }, @@ -1112,7 +1112,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null }, @@ -1161,7 +1161,7 @@ "success": false, "pending": true, "failed": false, - "checking_id": null, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "fee_msat": null, "preimage": null },