fix: phoenixd pending payments (#3075)

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
dni ⚡ 2025-07-04 11:01:50 +02:00
parent 88cf1ac853
commit 90ab642dcd
No known key found for this signature in database
GPG key ID: D1F416F29AD26E87
2 changed files with 102 additions and 49 deletions

View file

@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator
from typing import Any, Optional from typing import Any, Optional
import httpx import httpx
from httpx import RequestError, TimeoutException
from loguru import logger from loguru import logger
from websockets.legacy.client import connect from websockets.legacy.client import connect
@ -15,6 +16,7 @@ from lnbits.settings import settings
from .base import ( from .base import (
InvoiceResponse, InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus, PaymentPendingStatus,
PaymentResponse, PaymentResponse,
PaymentStatus, PaymentStatus,
@ -173,13 +175,29 @@ class PhoenixdWallet(Wallet):
) )
r.raise_for_status() 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() data = r.json()
if "routingFeeSat" not in data and "reason" in data: if "routingFeeSat" not in data and ("reason" in data or "message" in data):
return PaymentResponse(error_message=data["reason"]) error_message = data.get("reason", data.get("message", "Unknown error"))
if r.is_error or "paymentHash" not in data:
error_message = data["message"] if "message" in data else r.text
return PaymentResponse(error_message=error_message) return PaymentResponse(error_message=error_message)
checking_id = data["paymentHash"] checking_id = data["paymentHash"]
@ -208,38 +226,73 @@ class PhoenixdWallet(Wallet):
) )
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try: r = await self.client.get(
r = await self.client.get(f"/payments/incoming/{checking_id}") f"/payments/incoming/{checking_id}?all=true&limit=1000"
)
if r.is_error: 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() return PaymentPendingStatus()
try:
data = r.json() data = r.json()
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()
if data["isPaid"]: 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"] fee_msat = data["fees"]
preimage = data["preimage"] preimage = data["preimage"]
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
return PaymentPendingStatus() return PaymentPendingStatus()
except Exception as e:
logger.error(f"Error getting invoice status: {e}")
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus: async def get_payment_status(self, checking_id: str) -> PaymentStatus:
# TODO: how can we detect a failed payment? r = await self.client.get(
try: f"/payments/outgoing/{checking_id}?all=true&limit=1000"
r = await self.client.get(f"/payments/outgoing/{checking_id}") )
if r.is_error: 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() return PaymentPendingStatus()
try:
data = r.json() data = r.json()
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()
if data["isPaid"]: 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"] fee_msat = data["fees"]
preimage = data["preimage"] preimage = data["preimage"]
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
return PaymentPendingStatus()
except Exception as e:
logger.error(f"Error getting invoice status: {e}")
return PaymentPendingStatus() return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
@ -263,7 +316,7 @@ class PhoenixdWallet(Wallet):
yield message_json["paymentHash"] yield message_json["paymentHash"]
except Exception as exc: except Exception as exc:
logger.error( logger.warning(
f"lost connection to phoenixd invoices stream: '{exc}'" f"lost connection to phoenixd invoices stream: '{exc}'"
"retrying in 5 seconds" "retrying in 5 seconds"
) )

View file

@ -2182,8 +2182,16 @@
"get_invoice_status_endpoint": [] "get_invoice_status_endpoint": []
}, },
"phoenixd": { "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", "description": "bad json",
"response_type": "data", "response_type": "data",
"response": "data-not-json" "response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
} }
] ]
} }
@ -2674,8 +2674,16 @@
] ]
}, },
"phoenixd": { "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", "description": "bad json",
"response_type": "data", "response_type": "data",
"response": "data-not-json" "response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
} }
] ]
} }