mirror of
https://github.com/lnbits/lnbits.git
synced 2026-08-16 13:10:15 +02:00
clean up funding source ambiguity
This commit is contained in:
parent
9c11b156b3
commit
290eba33fa
18 changed files with 468 additions and 176 deletions
|
|
@ -851,7 +851,7 @@ async def _pay_external_invoice(
|
|||
# IMPORTANT PAYMENT RULES!
|
||||
# True -> success
|
||||
# False-> failed
|
||||
# None -> pending (any additional ambigous payments MUST be set as pending)
|
||||
# None -> pending (any ambigous payment responses MUST be set as pending)
|
||||
|
||||
# payment failed
|
||||
if payment_response.failed:
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class AlbyWallet(Wallet):
|
|||
# - https://api.getalby.com/invoices/incoming
|
||||
# - https://api.getalby.com/invoices/outgoing
|
||||
return PaymentStatus(
|
||||
statuses[data.get("state")], fee_msat=None, preimage=None
|
||||
statuses.get(data.get("state")), fee_msat=None, preimage=None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice status: {e}")
|
||||
|
|
|
|||
|
|
@ -179,14 +179,15 @@ class BlinkWallet(Wallet):
|
|||
try:
|
||||
response = await self._graphql_query(data)
|
||||
|
||||
errors = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoicePaymentSend", {})
|
||||
.get("errors", {})
|
||||
)
|
||||
payment_result = response.get("data", {}).get("lnInvoicePaymentSend", {})
|
||||
errors = payment_result.get("errors", {})
|
||||
if len(errors) > 0:
|
||||
error_message = errors[0].get("message")
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
status = payment_result.get("status")
|
||||
return PaymentResponse(
|
||||
ok=False if status in {"FAILURE", "FAILED"} else None,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
checking_id = bolt11_lib.decode(bolt11).payment_hash
|
||||
|
||||
|
|
@ -194,7 +195,10 @@ class BlinkWallet(Wallet):
|
|||
fee_msat = payment_status.fee_msat
|
||||
preimage = payment_status.preimage
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
ok=payment_status.paid,
|
||||
checking_id=checking_id,
|
||||
fee_msat=fee_msat,
|
||||
preimage=preimage,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class BoltzWallet(Wallet):
|
|||
return PaymentResponse(ok=True, checking_id=invoice.payment_hash)
|
||||
except AioRpcError as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(ok=False, error_message=exc.details())
|
||||
return PaymentResponse(error_message=exc.details())
|
||||
|
||||
try:
|
||||
info_request = boltzrpc_pb2.GetSwapInfoRequest(id=response.id)
|
||||
|
|
@ -188,12 +188,10 @@ class BoltzWallet(Wallet):
|
|||
)
|
||||
elif info.swap.error != "":
|
||||
return PaymentResponse(ok=False, error_message=info.swap.error)
|
||||
return PaymentResponse(
|
||||
ok=False, error_message="stream stopped unexpectedly"
|
||||
)
|
||||
return PaymentResponse(error_message="stream stopped unexpectedly")
|
||||
except AioRpcError as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(ok=False, error_message=exc.details())
|
||||
return PaymentResponse(error_message=exc.details())
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
|
|
@ -217,10 +215,14 @@ class BoltzWallet(Wallet):
|
|||
fee_msat=fee_msat,
|
||||
preimage=swap.preimage,
|
||||
)
|
||||
elif swap.state == boltzrpc_pb2.SwapState.PENDING:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
return PaymentFailedStatus()
|
||||
if swap.state in {
|
||||
boltzrpc_pb2.SwapState.ERROR,
|
||||
boltzrpc_pb2.SwapState.SERVER_ERROR,
|
||||
boltzrpc_pb2.SwapState.REFUNDED,
|
||||
boltzrpc_pb2.SwapState.ABANDONED,
|
||||
}:
|
||||
return PaymentFailedStatus()
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
|
|
@ -231,7 +233,7 @@ class BoltzWallet(Wallet):
|
|||
metadata=self.metadata,
|
||||
)
|
||||
swap = response.swap
|
||||
except AioRpcError as exc:
|
||||
except (AioRpcError, ValueError) as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentPendingStatus()
|
||||
if swap.state == boltzrpc_pb2.SwapState.SUCCESSFUL:
|
||||
|
|
@ -243,10 +245,14 @@ class BoltzWallet(Wallet):
|
|||
fee_msat=fee_msat,
|
||||
preimage=swap.preimage,
|
||||
)
|
||||
elif swap.state == boltzrpc_pb2.SwapState.PENDING:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
return PaymentFailedStatus()
|
||||
if swap.state in {
|
||||
boltzrpc_pb2.SwapState.ERROR,
|
||||
boltzrpc_pb2.SwapState.SERVER_ERROR,
|
||||
boltzrpc_pb2.SwapState.REFUNDED,
|
||||
boltzrpc_pb2.SwapState.ABANDONED,
|
||||
}:
|
||||
return PaymentFailedStatus()
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while settings.lnbits_running:
|
||||
|
|
|
|||
|
|
@ -104,41 +104,42 @@ class ClicheWallet(Wallet):
|
|||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
ws = create_connection(self.endpoint)
|
||||
ws.send(f"pay-invoice --invoice {bolt11}")
|
||||
checking_id, fee_msat, preimage, payment_ok = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
for _ in range(2):
|
||||
r = ws.recv()
|
||||
data = json.loads(r)
|
||||
try:
|
||||
ws = create_connection(self.endpoint)
|
||||
ws.send(f"pay-invoice --invoice {bolt11}")
|
||||
checking_id, fee_msat, preimage, payment_ok = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
for _ in range(2):
|
||||
r = ws.recv()
|
||||
data = json.loads(r)
|
||||
|
||||
if data.get("error") is not None:
|
||||
error_message = data["error"].get("message")
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
if data.get("error") is not None:
|
||||
error_message = data["error"].get("message")
|
||||
return PaymentResponse(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("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")
|
||||
if data.get("result") is None:
|
||||
return PaymentResponse(error_message="result is None")
|
||||
|
||||
return PaymentResponse(
|
||||
ok=payment_ok, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
return PaymentResponse(
|
||||
ok=payment_ok,
|
||||
checking_id=checking_id,
|
||||
fee_msat=fee_msat,
|
||||
preimage=preimage,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message=f"Unable to query {self.endpoint}.")
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
ws = create_connection(self.endpoint)
|
||||
|
|
@ -154,21 +155,25 @@ class ClicheWallet(Wallet):
|
|||
return PaymentStatus(statuses[data["result"]["status"]])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
ws = create_connection(self.endpoint)
|
||||
ws.send(f"check-payment --hash {checking_id}")
|
||||
r = ws.recv()
|
||||
data = json.loads(r)
|
||||
try:
|
||||
ws = create_connection(self.endpoint)
|
||||
ws.send(f"check-payment --hash {checking_id}")
|
||||
r = ws.recv()
|
||||
data = json.loads(r)
|
||||
|
||||
if data.get("error") is not None and data["error"].get("message"):
|
||||
logger.error(data["error"]["message"])
|
||||
if data.get("error") is not None and data["error"].get("message"):
|
||||
logger.error(data["error"]["message"])
|
||||
return PaymentPendingStatus()
|
||||
payment = data["result"]
|
||||
statuses = {"pending": None, "complete": True, "failed": False}
|
||||
return PaymentStatus(
|
||||
statuses.get(payment.get("status")),
|
||||
payment.get("fee_msatoshi"),
|
||||
payment.get("preimage"),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentPendingStatus()
|
||||
payment = data["result"]
|
||||
statuses = {"pending": None, "complete": True, "failed": False}
|
||||
return PaymentStatus(
|
||||
statuses[payment["status"]],
|
||||
payment.get("fee_msatoshi"),
|
||||
payment.get("preimage"),
|
||||
)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while settings.lnbits_running:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from lnbits.utils.crypto import random_secret_and_hash
|
|||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
|
|
@ -382,6 +383,8 @@ class CLNRestWallet(Wallet):
|
|||
if pay["status"] == "complete":
|
||||
fee_msat = pay["amount_sent_msat"] - pay["amount_msat"]
|
||||
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=pay["preimage"])
|
||||
if pay["status"] == "failed":
|
||||
return PaymentFailedStatus()
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning(f"Error getting payment status: {exc}")
|
||||
|
|
|
|||
|
|
@ -201,7 +201,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'",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -105,44 +105,58 @@ 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:
|
||||
r = await self.client.post(
|
||||
f"/wallet/{self.wallet_key}/withdraw",
|
||||
json={"payment_request": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message="Unable to connect to LNPay.")
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
except Exception:
|
||||
return PaymentResponse(ok=False, error_message="Got invalid JSON.")
|
||||
return PaymentResponse(error_message="Got invalid JSON.")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentResponse(ok=False, error_message=data["message"])
|
||||
return PaymentResponse(
|
||||
ok=False if r.is_client_error else None,
|
||||
error_message=data.get("message", r.text),
|
||||
)
|
||||
|
||||
checking_id = data["lnTx"]["id"]
|
||||
fee_msat = 0
|
||||
preimage = data["lnTx"]["payment_preimage"]
|
||||
try:
|
||||
checking_id = data["lnTx"]["id"]
|
||||
preimage = data["lnTx"]["payment_preimage"]
|
||||
except (KeyError, TypeError):
|
||||
return PaymentResponse(
|
||||
error_message="LNPay response is missing required payment fields."
|
||||
)
|
||||
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:
|
||||
r = await self.client.get(
|
||||
url=f"/lntx/{checking_id}",
|
||||
)
|
||||
try:
|
||||
r = await self.client.get(
|
||||
url=f"/lntx/{checking_id}",
|
||||
)
|
||||
if r.is_error:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if r.is_error:
|
||||
data = r.json()
|
||||
paid = {0: None, 1: True, -1: False}.get(data.get("settled"))
|
||||
return PaymentStatus(
|
||||
paid, data.get("fee_msat"), data.get("payment_preimage")
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
data = r.json()
|
||||
preimage = data["payment_preimage"]
|
||||
fee_msat = data["fee_msat"]
|
||||
statuses = {0: None, 1: True, -1: False}
|
||||
return PaymentStatus(statuses[data["settled"]], fee_msat, preimage)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
while settings.lnbits_running:
|
||||
|
|
|
|||
|
|
@ -103,26 +103,43 @@ 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:
|
||||
r = await self.client.post(
|
||||
"/api/v1/payinvoice",
|
||||
json={"pay_req": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
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,
|
||||
error_message=r.text,
|
||||
)
|
||||
|
||||
if "error" in r.json():
|
||||
try:
|
||||
data = r.json()
|
||||
error_message = data["error"]
|
||||
except Exception:
|
||||
error_message = r.text
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
try:
|
||||
response = r.json()
|
||||
except json.JSONDecodeError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'invalid json response'"
|
||||
)
|
||||
|
||||
data = r.json()["details"]
|
||||
checking_id = data["payment_hash"]
|
||||
fee_msat = -data["fee"]
|
||||
preimage = data["preimage"]
|
||||
if "error" in response:
|
||||
error_message = response.get("error") or r.text
|
||||
return PaymentResponse(error_message=error_message)
|
||||
|
||||
try:
|
||||
data = response["details"]
|
||||
checking_id = data["payment_hash"]
|
||||
fee_msat = -data["fee"]
|
||||
preimage = data["preimage"]
|
||||
except (KeyError, TypeError):
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
)
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
|
|
|
|||
|
|
@ -625,8 +625,6 @@ class NWCWallet(Wallet):
|
|||
"QUOTA_EXCEEDED",
|
||||
"RESTRICTED",
|
||||
"UNAUTHORIZED",
|
||||
"INTERNAL",
|
||||
"OTHER",
|
||||
"PAYMENT_FAILED",
|
||||
]
|
||||
failed = e.code in failure_codes
|
||||
|
|
|
|||
|
|
@ -100,24 +100,29 @@ 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:
|
||||
r = await self.client.post(
|
||||
"/v2/withdrawals",
|
||||
json={"type": "ln", "address": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
logger.warning(error_message)
|
||||
return PaymentResponse(ok=None, error_message=error_message)
|
||||
if r.is_error:
|
||||
error_message = r.json().get("message", r.text)
|
||||
logger.warning(error_message)
|
||||
return PaymentResponse(ok=None, 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)
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"]
|
||||
fee_msat = -data["fee"] * 1000
|
||||
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)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message="Invalid OpenNode payment response.")
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"/v1/charge/{checking_id}")
|
||||
|
|
@ -128,22 +133,26 @@ 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}")
|
||||
try:
|
||||
r = await self.client.get(f"/v1/withdrawal/{checking_id}")
|
||||
if r.is_error:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if r.is_error:
|
||||
data = r.json()["data"]
|
||||
statuses = {
|
||||
"initial": None,
|
||||
"pending": None,
|
||||
"confirmed": True,
|
||||
"error": None,
|
||||
"failed": False,
|
||||
}
|
||||
fee = data.get("fee")
|
||||
fee_msat = -fee * 1000 if fee is not None else None
|
||||
return PaymentStatus(statuses.get(data.get("status")), fee_msat)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
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)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
while settings.lnbits_running:
|
||||
|
|
|
|||
|
|
@ -208,11 +208,11 @@ class PhoenixdWallet(Wallet):
|
|||
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
|
||||
# RequestError can also be raised after the server received the request.
|
||||
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, error_message=msg)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
|
|
|
|||
|
|
@ -162,7 +162,10 @@ class SparkWallet(Wallet):
|
|||
)
|
||||
|
||||
except (SparkError, UnknownError) as exc:
|
||||
listpays = await self.listpays(bolt11)
|
||||
try:
|
||||
listpays = await self.listpays(bolt11)
|
||||
except (SparkError, UnknownError):
|
||||
return PaymentResponse(error_message=str(exc))
|
||||
if not listpays:
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
|
||||
|
|
@ -175,10 +178,12 @@ class SparkWallet(Wallet):
|
|||
payment_hash = pay["payment_hash"]
|
||||
|
||||
if len(pays) > 1:
|
||||
raise SparkError(
|
||||
f"listpays({payment_hash}) returned an unexpected response:"
|
||||
f" {listpays}"
|
||||
) from exc
|
||||
return PaymentResponse(
|
||||
error_message=(
|
||||
f"listpays({payment_hash}) returned an unexpected response:"
|
||||
f" {listpays}"
|
||||
)
|
||||
)
|
||||
|
||||
if pay["status"] == "failed":
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
|
|
@ -203,7 +208,7 @@ class SparkWallet(Wallet):
|
|||
preimage=preimage,
|
||||
)
|
||||
else:
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
return PaymentResponse(error_message=str(exc))
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
|
|
@ -249,7 +254,8 @@ class SparkWallet(Wallet):
|
|||
if status == "failed":
|
||||
return PaymentFailedStatus()
|
||||
return PaymentPendingStatus()
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
logger.warning(f"supplied an invalid checking_id: {checking_id}")
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = f"/stream?access-key={self.token}"
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ class SparkL2Wallet(Wallet):
|
|||
checking_id = res.get("checking_id")
|
||||
if not checking_id:
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
error_message="Spark sidecar payment response missing checking_id.",
|
||||
)
|
||||
status = res.get("status")
|
||||
|
|
@ -178,7 +177,7 @@ class SparkL2Wallet(Wallet):
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
return PaymentResponse(ok=False, error_message=str(e))
|
||||
return PaymentResponse(error_message=str(e))
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -243,10 +243,14 @@ class StrikeWallet(Wallet):
|
|||
if error or not quote_id:
|
||||
return PaymentResponse(ok=False, error_message=error or "Unknown error")
|
||||
|
||||
# Keep the quote id before execution so an ambiguous execute response can
|
||||
# still be reconciled while this process is running.
|
||||
self.pending_payments[payment_hash] = quote_id
|
||||
|
||||
# 2) Execute the payment quote
|
||||
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(error_message=error or "Unknown error")
|
||||
|
||||
state = data.get("state", "").upper()
|
||||
payment_id = data.get("paymentId")
|
||||
|
|
@ -276,9 +280,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,7 +290,6 @@ class StrikeWallet(Wallet):
|
|||
f"body: {http_exc.response.text}"
|
||||
)
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
error_message=f"Strike API error: {http_exc.response.status_code}",
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
@ -625,7 +625,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()
|
||||
|
||||
|
|
@ -667,7 +667,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()
|
||||
|
||||
|
|
@ -693,10 +693,10 @@ class StrikeWallet(Wallet):
|
|||
continue
|
||||
logger.warning(
|
||||
f"Payment '{checking_id}' not a valid Strike payment. "
|
||||
f"Marked as failed. Response: {r_payment.text}"
|
||||
"Keeping pending because it may be the invoice payment "
|
||||
f"hash fallback. Response: {r_payment.text}"
|
||||
)
|
||||
self.pending_payments.pop(checking_id, None)
|
||||
return PaymentFailedStatus()
|
||||
return PaymentPendingStatus()
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,28 +105,41 @@ 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:
|
||||
r = await self.client.post(
|
||||
"payments",
|
||||
json={
|
||||
"invoice": bolt11,
|
||||
"description": "",
|
||||
"amount": "",
|
||||
"internalId": "",
|
||||
"callbackUrl": "",
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message="Unable to query ZBD.")
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
try:
|
||||
error_message = r.json().get("message", r.text)
|
||||
except Exception:
|
||||
error_message = r.text
|
||||
return PaymentResponse(
|
||||
ok=False if r.is_client_error else None,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
try:
|
||||
data = r.json()
|
||||
fee_msat = -int(data["data"]["fee"])
|
||||
preimage = data["data"]["preimage"]
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message="Invalid ZBD payment response.")
|
||||
|
||||
checking_id = bolt11_decode(bolt11).payment_hash
|
||||
fee_msat = -int(data["data"]["fee"])
|
||||
preimage = data["data"]["preimage"]
|
||||
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
|
|
@ -147,11 +160,20 @@ class ZBDWallet(Wallet):
|
|||
return PaymentStatus(paid=statuses[data.get("status")])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"payments/{checking_id}")
|
||||
try:
|
||||
r = await self.client.get(f"payments/{checking_id}")
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if r.is_error:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
data = r.json()["data"]
|
||||
try:
|
||||
data = r.json()["data"]
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
statuses = {
|
||||
"initial": None,
|
||||
|
|
@ -161,8 +183,7 @@ class ZBDWallet(Wallet):
|
|||
"expired": False,
|
||||
"failed": False,
|
||||
}
|
||||
|
||||
return PaymentStatus(paid=statuses[data.get("status")])
|
||||
return PaymentStatus(paid=statuses.get(data.get("status")))
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
|
|
|||
|
|
@ -379,18 +379,23 @@ async def test_retry_failed_invoice(
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("returns_checking_id", [True, False])
|
||||
async def test_pay_external_invoice_pending(
|
||||
from_wallet: Wallet,
|
||||
mocker: MockerFixture,
|
||||
external_funding_source: FakeWallet,
|
||||
settings: Settings,
|
||||
returns_checking_id: bool,
|
||||
):
|
||||
settings.lnbits_reserve_fee_min = 1000 # msats
|
||||
invoice_amount = 2103
|
||||
external_invoice = await external_funding_source.create_invoice(invoice_amount)
|
||||
assert external_invoice.payment_request
|
||||
assert external_invoice.checking_id
|
||||
backend_checking_id = f"backend_{external_invoice.checking_id}"
|
||||
backend_checking_id = (
|
||||
f"backend_{external_invoice.checking_id}" if returns_checking_id else None
|
||||
)
|
||||
expected_checking_id = backend_checking_id or external_invoice.checking_id
|
||||
|
||||
payment_reponse_pending = PaymentResponse(ok=None, checking_id=backend_checking_id)
|
||||
mocker.patch(
|
||||
|
|
@ -416,9 +421,9 @@ async def test_pay_external_invoice_pending(
|
|||
_payment = await get_standalone_payment(payment.payment_hash)
|
||||
assert _payment
|
||||
assert _payment.status == PaymentState.PENDING.value
|
||||
assert _payment.checking_id == backend_checking_id
|
||||
assert _payment.checking_id == expected_checking_id
|
||||
assert _payment.payment_hash == external_invoice.checking_id
|
||||
assert payment.checking_id == backend_checking_id
|
||||
assert payment.checking_id == expected_checking_id
|
||||
assert _payment.amount == -2103_000
|
||||
assert _payment.bolt11 == external_invoice.payment_request
|
||||
|
||||
|
|
|
|||
205
tests/unit/test_wallet_payment_ambiguity.py
Normal file
205
tests/unit/test_wallet_payment_ambiguity.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from pytest_mock.plugin import MockerFixture
|
||||
|
||||
from lnbits.wallets.base import PaymentPendingStatus
|
||||
from lnbits.wallets.blink import BlinkWallet
|
||||
from lnbits.wallets.boltz import BoltzWallet
|
||||
from lnbits.wallets.boltz_grpc_files import boltzrpc_pb2
|
||||
from lnbits.wallets.lndrest import LndRestWallet
|
||||
from lnbits.wallets.lnpay import LNPayWallet
|
||||
from lnbits.wallets.nwc import NWCError, NWCWallet
|
||||
from lnbits.wallets.phoenixd import PhoenixdWallet
|
||||
from lnbits.wallets.sparkl2 import SparkL2Wallet
|
||||
from lnbits.wallets.strike import StrikeWallet
|
||||
|
||||
|
||||
def _response(status_code: int, **kwargs) -> httpx.Response:
|
||||
request = httpx.Request("POST", "https://wallet.test/pay")
|
||||
return httpx.Response(status_code, request=request, **kwargs)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_blink_keeps_unconfirmed_payment_pending(mocker: MockerFixture):
|
||||
wallet = object.__new__(BlinkWallet)
|
||||
wallet._wallet_id = "wallet-id"
|
||||
wallet.endpoint = "https://wallet.test"
|
||||
mocker.patch(
|
||||
"lnbits.wallets.blink.bolt11_lib.decode",
|
||||
return_value=SimpleNamespace(payment_hash="payment-hash"),
|
||||
)
|
||||
mocker.patch.object(
|
||||
wallet,
|
||||
"_graphql_query",
|
||||
return_value={"data": {"lnInvoicePaymentSend": {"errors": []}}},
|
||||
)
|
||||
mocker.patch.object(
|
||||
wallet, "get_payment_status", return_value=PaymentPendingStatus()
|
||||
)
|
||||
|
||||
response = await wallet.pay_invoice("bolt11", 1_000)
|
||||
|
||||
assert response.ok is None
|
||||
assert response.checking_id == "payment-hash"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_lndrest_unknown_payment_state_is_pending(
|
||||
mocker: MockerFixture, settings
|
||||
):
|
||||
settings.lnd_rest_allow_self_payment = False
|
||||
wallet = object.__new__(LndRestWallet)
|
||||
cast(Any, wallet).client = SimpleNamespace(
|
||||
post=mocker.AsyncMock(
|
||||
return_value=_response(
|
||||
200,
|
||||
json={
|
||||
"result": {
|
||||
"status": "FUTURE_STATUS",
|
||||
"payment_hash": "payment-hash",
|
||||
"payment_preimage": "",
|
||||
"fee_msat": "0",
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
response = await wallet.pay_invoice("bolt11", 1_000)
|
||||
|
||||
assert response.ok is None
|
||||
assert response.checking_id == "payment-hash"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "expected"),
|
||||
[(400, False), (500, None)],
|
||||
)
|
||||
async def test_lnpay_only_treats_client_rejection_as_failed(
|
||||
mocker: MockerFixture, status_code: int, expected: bool | None
|
||||
):
|
||||
wallet = object.__new__(LNPayWallet)
|
||||
wallet.wallet_key = "wallet-key"
|
||||
cast(Any, wallet).client = SimpleNamespace(
|
||||
post=mocker.AsyncMock(
|
||||
return_value=_response(status_code, json={"message": "error"})
|
||||
)
|
||||
)
|
||||
|
||||
response = await wallet.pay_invoice("bolt11", 1_000)
|
||||
|
||||
assert response.ok is expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_lnpay_malformed_payment_response_is_pending(mocker: MockerFixture):
|
||||
wallet = object.__new__(LNPayWallet)
|
||||
wallet.wallet_key = "wallet-key"
|
||||
cast(Any, wallet).client = SimpleNamespace(
|
||||
post=mocker.AsyncMock(return_value=_response(200, content=b"not-json"))
|
||||
)
|
||||
|
||||
response = await wallet.pay_invoice("bolt11", 1_000)
|
||||
|
||||
assert response.ok is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("code", "expected"),
|
||||
[("PAYMENT_FAILED", False), ("INTERNAL", None), ("OTHER", None)],
|
||||
)
|
||||
async def test_nwc_only_explicit_payment_failure_is_failed(
|
||||
mocker: MockerFixture, code: str, expected: bool | None
|
||||
):
|
||||
wallet = object.__new__(NWCWallet)
|
||||
cast(Any, wallet).conn = SimpleNamespace(
|
||||
call=mocker.AsyncMock(side_effect=NWCError(code, "error"))
|
||||
)
|
||||
|
||||
response = await wallet.pay_invoice("bolt11", 1_000)
|
||||
|
||||
assert response.ok is expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_phoenix_request_error_is_pending(mocker: MockerFixture):
|
||||
wallet = object.__new__(PhoenixdWallet)
|
||||
wallet.endpoint = "https://wallet.test"
|
||||
request = httpx.Request("POST", "https://wallet.test/payinvoice")
|
||||
cast(Any, wallet).client = SimpleNamespace(
|
||||
post=mocker.AsyncMock(
|
||||
side_effect=httpx.ReadError("read failed", request=request)
|
||||
)
|
||||
)
|
||||
|
||||
response = await wallet.pay_invoice("bolt11", 1_000)
|
||||
|
||||
assert response.ok is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_spark_sidecar_missing_checking_id_is_pending(mocker: MockerFixture):
|
||||
wallet = object.__new__(SparkL2Wallet)
|
||||
mocker.patch.object(wallet, "_request", return_value={"status": "PENDING"})
|
||||
|
||||
response = await wallet.pay_invoice("not-a-bolt11", 1_000)
|
||||
|
||||
assert response.ok is None
|
||||
assert response.checking_id is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("state", "expected"),
|
||||
[
|
||||
(boltzrpc_pb2.SwapState.ERROR, False),
|
||||
(999, None),
|
||||
],
|
||||
)
|
||||
async def test_boltz_only_known_terminal_swap_state_is_failed(
|
||||
mocker: MockerFixture, state: int, expected: bool | None
|
||||
):
|
||||
wallet = object.__new__(BoltzWallet)
|
||||
wallet.metadata = None
|
||||
cast(Any, wallet).rpc = SimpleNamespace(
|
||||
GetSwapInfo=mocker.AsyncMock(
|
||||
return_value=SimpleNamespace(swap=SimpleNamespace(state=state))
|
||||
)
|
||||
)
|
||||
|
||||
status = await wallet.get_payment_status("00" * 32)
|
||||
|
||||
assert status.paid is expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_strike_invalid_fallback_identifier_is_pending(mocker: MockerFixture):
|
||||
wallet = object.__new__(StrikeWallet)
|
||||
wallet.pending_payments = {}
|
||||
cast(Any, wallet)._get = mocker.AsyncMock(
|
||||
return_value=_response(
|
||||
400,
|
||||
json={
|
||||
"data": {
|
||||
"code": "INVALID_DATA",
|
||||
"validationErrors": {
|
||||
"paymentId": [
|
||||
{
|
||||
"code": "INVALID_DATA",
|
||||
"message": "paymentId is not valid.",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
status = await wallet._get_payment_status_by_checking_id("payment-hash")
|
||||
|
||||
assert status.paid is None
|
||||
Loading…
Add table
Add a link
Reference in a new issue