diff --git a/lnbits/wallets/phoenixd.py b/lnbits/wallets/phoenixd.py index 8413881a1..84e0d00b2 100644 --- a/lnbits/wallets/phoenixd.py +++ b/lnbits/wallets/phoenixd.py @@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator from typing import Any, Optional import httpx +from httpx import RequestError, TimeoutException from loguru import logger from websockets.legacy.client import connect @@ -15,6 +16,7 @@ from lnbits.settings import settings from .base import ( InvoiceResponse, + PaymentFailedStatus, PaymentPendingStatus, PaymentResponse, PaymentStatus, @@ -173,13 +175,29 @@ class PhoenixdWallet(Wallet): ) r.raise_for_status() + + except TimeoutException: + # 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) + 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) + except Exception as exc: + logger.warning(exc) + return PaymentResponse( + ok=None, error_message=f"Unable to connect to {self.endpoint}." + ) + + try: data = r.json() - if "routingFeeSat" not in data and "reason" in data: - return PaymentResponse(error_message=data["reason"]) - - if r.is_error or "paymentHash" not in data: - error_message = data["message"] if "message" in data else r.text + 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) checking_id = data["paymentHash"] @@ -208,40 +226,75 @@ class PhoenixdWallet(Wallet): ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: - try: - r = await self.client.get(f"/payments/incoming/{checking_id}") - if r.is_error: + r = await self.client.get( + f"/payments/incoming/{checking_id}?all=true&limit=1000" + ) + if r.is_error: + if r.status_code == 404: + # invoice does not exist in phoenixd, so it was never paid + return PaymentFailedStatus() + else: + # otherwise something unexpected happened, and we keep it pending + logger.warning( + f"Error getting invoice status: {r.text}, keeping pending" + ) return PaymentPendingStatus() + try: data = r.json() - - if data["isPaid"]: - fee_msat = data["fees"] - preimage = data["preimage"] - return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) - + except json.JSONDecodeError: + # should never return invalid json, but just in case we keep it pending + logger.warning( + f"Phoenixd: invalid json response: {r.text}, keeping pending" + ) return PaymentPendingStatus() - except Exception as e: - logger.error(f"Error getting invoice status: {e}") + + if "isPaid" not in data or "fees" not in data or "preimage" not in data: + # should never return missing fields, but just in case we keep it pending return PaymentPendingStatus() + if data["isPaid"] is True: + fee_msat = data["fees"] + preimage = data["preimage"] + return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) + + return PaymentPendingStatus() + async def get_payment_status(self, checking_id: str) -> PaymentStatus: - # TODO: how can we detect a failed payment? - try: - r = await self.client.get(f"/payments/outgoing/{checking_id}") - if r.is_error: + r = await self.client.get( + f"/payments/outgoing/{checking_id}?all=true&limit=1000" + ) + if r.is_error: + if r.status_code == 404: + # payment does not exist in phoenixd, so it was never paid + return PaymentFailedStatus() + else: + # otherwise something unexpected happened, and we keep it pending + logger.warning( + f"Error getting payment status: {r.text}, keeping pending" + ) return PaymentPendingStatus() + try: data = r.json() - - if data["isPaid"]: - fee_msat = data["fees"] - preimage = data["preimage"] - return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) - + except json.JSONDecodeError: + # should never return invalid json, but just in case we keep it pending + logger.warning( + f"Phoenixd: invalid json response: {r.text}, keeping pending" + ) return PaymentPendingStatus() - except Exception as e: - logger.error(f"Error getting invoice status: {e}") + + if "isPaid" not in data or "fees" not in data or "preimage" not in data: + # should never happen, but just in case we keep it pending + logger.warning( + f"Phoenixd: missing required fields: {data}, keeping pending" + ) return PaymentPendingStatus() + if data["isPaid"] is True: + fee_msat = data["fees"] + preimage = data["preimage"] + return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) + return PaymentPendingStatus() + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while settings.lnbits_running: try: @@ -263,7 +316,7 @@ class PhoenixdWallet(Wallet): yield message_json["paymentHash"] except Exception as exc: - logger.error( + logger.warning( f"lost connection to phoenixd invoices stream: '{exc}'" "retrying in 5 seconds" ) diff --git a/tests/wallets/fixtures/json/fixtures_rest.json b/tests/wallets/fixtures/json/fixtures_rest.json index 8f9eb1b90..e6df5fd38 100644 --- a/tests/wallets/fixtures/json/fixtures_rest.json +++ b/tests/wallets/fixtures/json/fixtures_rest.json @@ -2182,8 +2182,16 @@ "get_invoice_status_endpoint": [] }, "phoenixd": { - "description": "phoenixd.py doesn't handle the 'failed' status for `get_invoice_status`", - "get_invoice_status_endpoint": [] + "get_invoice_status_endpoint": [ + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] } } }, @@ -2389,14 +2397,6 @@ "description": "bad json", "response_type": "data", "response": "data-not-json" - }, - { - "description": "http 404", - "response_type": "response", - "response": { - "response": "Not Found", - "status": 404 - } } ] } @@ -2674,8 +2674,16 @@ ] }, "phoenixd": { - "description": "phoenixd.py doesn't handle the 'failed' status for `get_invoice_status`", - "get_payment_status_endpoint": [] + "get_payment_status_endpoint": [ + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] } } }, @@ -2932,14 +2940,6 @@ "description": "bad json", "response_type": "data", "response": "data-not-json" - }, - { - "description": "http 404", - "response_type": "response", - "response": { - "response": "Not Found", - "status": 404 - } } ] }