fix: payout hardening/remove ambiguity

This commit is contained in:
Arc 2026-07-27 17:21:09 +01:00
parent 9aedfdcc9b
commit aa62b44cdd
24 changed files with 645 additions and 256 deletions

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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",
)

View file

@ -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(

View file

@ -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

View file

@ -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}

View file

@ -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]

View file

@ -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(

View file

@ -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

View file

@ -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:

View file

@ -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.",
)

View file

@ -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'",
)

View file

@ -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)

View file

@ -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
)

View file

@ -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]

View file

@ -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)

View file

@ -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:

View file

@ -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":

View file

@ -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:

View file

@ -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()

View file

@ -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

View file

@ -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
}

View file

@ -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
},