fix: pay invoice status (#2481)

* fix: rest `pay_invoice` pending instead of failed
* fix: rpc `pay_invoice` pending instead of failed
* fix: return "failed" value for payment
* fix: handle failed status for LNbits funding source
* chore: `phoenixd` todo
* test: fix condition
* fix: wait for payment status to be updated
* fix: fail payment when explicit status provided

---------

Co-authored-by: dni  <office@dnilabs.com>
This commit is contained in:
Vlad Stan 2024-05-10 12:49:50 +03:00 committed by GitHub
parent b9e62bfceb
commit eae5002b69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 335 additions and 220 deletions

View file

@ -27,19 +27,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
- name: docker build
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
uses: docker/setup-buildx-action@v3
- name: Build and push
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: lnbits/lnbits:latest
cache-from: type=registry,ref=lnbits/lnbits:latest
cache-to: type=inline
run: |
docker build -t lnbits/lnbits .
- name: Setup Regtest
run: |
@ -89,3 +80,8 @@ jobs:
file: ./coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
- name: docker lnbits logs
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
run: |
docker logs lnbits-lnbits-1

View file

@ -63,11 +63,15 @@ from .models import Payment, UserConfig, Wallet
class PaymentError(Exception):
pass
def __init__(self, message: str, status: str = "pending"):
self.message = message
self.status = status
class InvoiceError(Exception):
pass
def __init__(self, message: str, status: str = "pending"):
self.message = message
self.status = status
async def calculate_fiat_amounts(
@ -123,11 +127,11 @@ async def create_invoice(
conn: Optional[Connection] = None,
) -> Tuple[str, str]:
if not amount > 0:
raise InvoiceError("Amountless invoices not supported.")
raise InvoiceError("Amountless invoices not supported.", status="failed")
user_wallet = await get_wallet(wallet_id, conn=conn)
if not user_wallet:
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.")
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
invoice_memo = None if description_hash else memo
@ -142,8 +146,9 @@ async def create_invoice(
user_wallet.balance_msat / 1000 + amount_sat
):
raise InvoiceError(
f"Wallet balance cannot exceed "
f"{settings.lnbits_wallet_limit_max_balance} sats."
f"Wallet balance cannot exceed "
f"{settings.lnbits_wallet_limit_max_balance} sats.",
status="failed",
)
(
@ -159,7 +164,9 @@ async def create_invoice(
expiry=expiry or settings.lightning_invoice_expiry,
)
if not ok or not payment_request or not checking_id:
raise InvoiceError(error_message or "unexpected backend error.")
raise InvoiceError(
error_message or "unexpected backend error.", status="pending"
)
invoice = bolt11_decode(payment_request)
@ -202,12 +209,12 @@ async def pay_invoice(
try:
invoice = bolt11_decode(payment_request)
except Exception as exc:
raise InvoiceError("Bolt11 decoding failed.") from exc
raise PaymentError("Bolt11 decoding failed.", status="failed") from exc
if not invoice.amount_msat or not invoice.amount_msat > 0:
raise InvoiceError("Amountless invoices not supported.")
raise PaymentError("Amountless invoices not supported.", status="failed")
if max_sat and invoice.amount_msat > max_sat * 1000:
raise InvoiceError("Amount in invoice is too high.")
raise PaymentError("Amount in invoice is too high.", status="failed")
await check_wallet_limits(wallet_id, conn, invoice.amount_msat)
@ -242,7 +249,7 @@ async def pay_invoice(
# we check if an internal invoice exists that has already been paid
# (not pending anymore)
if not await check_internal_pending(invoice.payment_hash, conn=conn):
raise PaymentError("Internal invoice already paid.")
raise PaymentError("Internal invoice already paid.", status="failed")
# check_internal() returns the checking_id of the invoice we're waiting for
# (pending only)
@ -261,7 +268,7 @@ async def pay_invoice(
internal_invoice.amount != invoice.amount_msat
or internal_invoice.bolt11 != payment_request.lower()
):
raise PaymentError("Invalid invoice.")
raise PaymentError("Invalid invoice.", status="failed")
logger.debug(f"creating temporary internal payment with id {internal_id}")
# create a new payment from this wallet
@ -289,7 +296,7 @@ async def pay_invoice(
except Exception as exc:
logger.error(f"could not create temporary payment: {exc}")
# happens if the same wallet tries to pay an invoice twice
raise PaymentError("Could not make payment.") from exc
raise PaymentError("Could not make payment.", status="failed") from exc
# do the balance check
wallet = await get_wallet(wallet_id, conn=conn)
@ -302,9 +309,10 @@ async def pay_invoice(
):
raise PaymentError(
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
" sat) to cover potential routing fees."
" sat) to cover potential routing fees.",
status="failed",
)
raise PermissionError("Insufficient balance.")
raise PaymentError("Insufficient balance.", status="failed")
if internal_checking_id:
service_fee_msat = service_fee(invoice.amount_msat, internal=True)
@ -340,6 +348,7 @@ async def pay_invoice(
)
logger.debug(f"backend: pay_invoice finished {temp_id}")
logger.debug(f"backend: pay_invoice response {payment}")
if payment.checking_id and payment.ok is not False:
# payment.ok can be True (paid) or None (pending)!
logger.debug(f"updating payment {temp_id}")
@ -370,7 +379,8 @@ async def pay_invoice(
await delete_wallet_payment(temp_id, wallet_id, conn=conn)
raise PaymentError(
f"Payment failed: {payment.error_message}"
or "Payment failed, but backend didn't give us an error message."
or "Payment failed, but backend didn't give us an error message.",
status="failed",
)
else:
logger.warning(
@ -413,8 +423,9 @@ async def check_time_limit_between_transactions(conn, wallet_id):
if len(payments) == 0:
return
raise ValueError(
f"The time limit of {limit} seconds between payments has been reached."
raise PaymentError(
status="failed",
message=f"The time limit of {limit} seconds between payments has been reached.",
)

View file

@ -171,7 +171,10 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
assert payment_db is not None, "payment not found"
checking_id = payment_db.checking_id
except InvoiceError as exc:
raise HTTPException(status_code=520, detail=str(exc)) from exc
return JSONResponse(
status_code=520,
content={"detail": exc.message, "status": exc.status},
)
except Exception as exc:
raise exc
@ -217,14 +220,11 @@ async def api_payments_pay_invoice(
payment_hash = await pay_invoice(
wallet_id=wallet.id, payment_request=bolt11, extra=extra
)
except ValueError as exc:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
) from exc
except PermissionError as exc:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=str(exc)) from exc
except PaymentError as exc:
raise HTTPException(status_code=520, detail=str(exc)) from exc
return JSONResponse(
status_code=520,
content={"detail": exc.message, "status": exc.status},
)
except Exception as exc:
raise exc
@ -434,7 +434,7 @@ async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
return {"paid": True, "preimage": payment.preimage}
try:
await payment.check_status()
status = await payment.check_status()
except Exception:
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
@ -443,6 +443,7 @@ async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
if wallet and wallet.id == payment.wallet_id:
return {
"paid": not payment.pending,
"status": f"{status!s}",
"preimage": payment.preimage,
"details": payment,
}

View file

@ -129,7 +129,7 @@ class AlbyWallet(Wallet):
if r.is_error:
error_message = data["message"] if "message" in data else r.text
return PaymentResponse(False, None, None, None, error_message)
return PaymentResponse(None, None, None, None, error_message)
checking_id = data["payment_hash"]
# todo: confirm with bitkarrot that having the minus is fine
@ -141,18 +141,18 @@ class AlbyWallet(Wallet):
except KeyError as exc:
logger.warning(exc)
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
None, None, None, None, "Server error: 'missing required fields'"
)
except json.JSONDecodeError as exc:
logger.warning(exc)
return PaymentResponse(
False, None, None, None, "Server error: 'invalid json response'"
None, None, None, None, "Server error: 'invalid json response'"
)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
return PaymentResponse(
False, None, None, None, f"Unable to connect to {self.endpoint}."
None, None, None, None, f"Unable to connect to {self.endpoint}."
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
@ -167,6 +167,7 @@ class AlbyWallet(Wallet):
data = r.json()
# TODO: how can we detect a failed payment?
statuses = {
"CREATED": None,
"SETTLED": True,

View file

@ -70,14 +70,11 @@ class PaymentStatus(NamedTuple):
return self.paid is False
def __str__(self) -> str:
if self.paid is True:
return "settled"
elif self.paid is False:
if self.success:
return "success"
if self.failed:
return "failed"
elif self.paid is None:
return "still pending"
else:
return "unknown (should never happen)"
return "pending"
class PaymentSuccessStatus(PaymentStatus):

View file

@ -46,6 +46,15 @@ class CoreLightningWallet(Wallet):
command = self.ln.help("invoice")["help"][0]["command"] # type: ignore
self.supports_description_hash = "deschashonly" in command
# https://docs.corelightning.org/reference/lightning-pay
# 201: Already paid
# 203: Permanent failure at destination.
# 205: Unable to find a route.
# 206: Route too expensive.
# 207: Invoice expired.
# 210: Payment timed out without a payment in progress.
self.pay_failure_error_codes = [201, 203, 205, 206, 207, 210]
# check last payindex so we can listen from that point on
self.last_pay_index = 0
invoices: dict = self.ln.listinvoices() # type: ignore
@ -155,19 +164,27 @@ class CoreLightningWallet(Wallet):
except RpcError as exc:
logger.warning(exc)
try:
error_message = exc.error["attempts"][-1]["fail_reason"] # type: ignore
error_code = exc.error.get("code")
if error_code in self.pay_failure_error_codes: # type: ignore
error_message = exc.error.get("message", error_code) # type: ignore
return PaymentResponse(
False, None, None, None, f"Payment failed: {error_message}"
)
else:
error_message = f"Payment failed: {exc.error}"
return PaymentResponse(None, None, None, None, error_message)
except Exception:
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
return PaymentResponse(False, None, None, None, error_message)
return PaymentResponse(None, None, None, None, error_message)
except KeyError as exc:
logger.warning(exc)
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
None, None, None, None, "Server error: 'missing required fields'"
)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
return PaymentResponse(False, None, None, None, f"Payment failed: '{exc}'.")
return PaymentResponse(None, None, None, None, f"Payment failed: '{exc}'.")
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:

View file

@ -49,6 +49,15 @@ class CoreLightningRestWallet(Wallet):
"User-Agent": settings.user_agent,
}
# https://docs.corelightning.org/reference/lightning-pay
# 201: Already paid
# 203: Permanent failure at destination.
# 205: Unable to find a route.
# 206: Route too expensive.
# 207: Invoice expired.
# 210: Payment timed out without a payment in progress.
self.pay_failure_error_codes = [201, 203, 205, 206, 207, 210]
self.cert = settings.corelightning_rest_cert or False
self.client = httpx.AsyncClient(verify=self.cert, headers=headers)
self.last_pay_index = 0
@ -176,37 +185,48 @@ class CoreLightningRestWallet(Wallet):
r.raise_for_status()
data = r.json()
if "error" in data:
return PaymentResponse(False, None, None, None, data["error"])
if r.is_error:
return PaymentResponse(False, None, None, None, r.text)
if (
"payment_hash" not in data
or "payment_preimage" not in data
or "msatoshi_sent" not in data
or "msatoshi" not in data
or "status" not in data
):
status = self.statuses.get(data["status"])
if "payment_preimage" not in data:
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
status,
None,
None,
None,
data.get("error"),
)
checking_id = data["payment_hash"]
preimage = data["payment_preimage"]
fee_msat = data["msatoshi_sent"] - data["msatoshi"]
return PaymentResponse(
self.statuses.get(data["status"]), checking_id, fee_msat, preimage, None
)
return PaymentResponse(status, checking_id, fee_msat, preimage, None)
except httpx.HTTPStatusError as exc:
try:
logger.debug(exc)
data = exc.response.json()
if data["error"]["code"] in self.pay_failure_error_codes: # type: ignore
error_message = f"Payment failed: {data['error']['message']}"
return PaymentResponse(False, None, None, None, error_message)
error_message = f"REST failed with {data['error']['message']}."
return PaymentResponse(None, None, None, None, error_message)
except Exception as exc:
error_message = f"Unable to connect to {self.url}."
return PaymentResponse(None, None, None, None, error_message)
except json.JSONDecodeError:
return PaymentResponse(
False, None, None, None, "Server error: 'invalid json response'"
None, None, None, None, "Server error: 'invalid json response'"
)
except KeyError as exc:
logger.warning(exc)
return PaymentResponse(
None, None, None, None, "Server error: 'missing required fields'"
)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
return PaymentResponse(
False, None, None, None, f"Unable to connect to {self.url}."
None, None, None, None, f"Unable to connect to {self.url}."
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:

View file

@ -142,9 +142,9 @@ class EclairWallet(Wallet):
data = r.json()
if "error" in data:
return PaymentResponse(False, None, None, None, data["error"])
return PaymentResponse(None, None, None, None, data["error"])
if r.is_error:
return PaymentResponse(False, None, None, None, r.text)
return PaymentResponse(None, None, None, None, r.text)
if data["type"] == "payment-failed":
return PaymentResponse(False, None, None, None, "payment failed")
@ -154,17 +154,17 @@ class EclairWallet(Wallet):
except json.JSONDecodeError:
return PaymentResponse(
False, None, None, None, "Server error: 'invalid json response'"
None, None, None, None, "Server error: 'invalid json response'"
)
except KeyError:
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
None, None, None, None, "Server error: 'missing required fields'"
)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
return PaymentResponse(
False, None, None, None, f"Unable to connect to {self.url}."
None, None, None, None, f"Unable to connect to {self.url}."
)
payment_status: PaymentStatus = await self.get_payment_status(checking_id)

View file

@ -9,6 +9,7 @@ from lnbits.settings import settings
from .base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
@ -115,13 +116,10 @@ class LNbitsWallet(Wallet):
json={"out": True, "bolt11": bolt11},
timeout=None,
)
r.raise_for_status()
data = r.json()
if r.is_error or "payment_hash" not in data:
error_message = data["detail"] if "detail" in data else r.text
return PaymentResponse(False, None, None, None, error_message)
checking_id = data["payment_hash"]
# we do this to get the fee and preimage
@ -131,19 +129,32 @@ class LNbitsWallet(Wallet):
return PaymentResponse(
success, checking_id, payment.fee_msat, payment.preimage
)
except httpx.HTTPStatusError as exc:
try:
logger.debug(exc)
data = exc.response.json()
error_message = f"Payment {data['status']}: {data['detail']}."
if data["status"] == "failed":
return PaymentResponse(False, None, None, None, error_message)
return PaymentResponse(None, None, None, None, error_message)
except Exception as exc:
error_message = f"Unable to connect to {self.endpoint}."
return PaymentResponse(None, None, None, None, error_message)
except json.JSONDecodeError:
return PaymentResponse(
False, None, None, None, "Server error: 'invalid json response'"
None, None, None, None, "Server error: 'invalid json response'"
)
except KeyError:
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
None, None, None, None, "Server error: 'missing required fields'"
)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
return PaymentResponse(
False, None, None, None, f"Unable to connect to {self.endpoint}."
None, None, None, None, f"Unable to connect to {self.endpoint}."
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
@ -169,6 +180,9 @@ class LNbitsWallet(Wallet):
return PaymentPendingStatus()
data = r.json()
if data.get("status") == "failed":
return PaymentFailedStatus()
if "paid" not in data or not data["paid"]:
return PaymentPendingStatus()

View file

@ -167,7 +167,7 @@ class LndWallet(Wallet):
resp = await self.routerpc.SendPaymentV2(req).read()
except Exception as exc:
logger.warning(exc)
return PaymentResponse(False, None, None, None, str(exc))
return PaymentResponse(None, None, None, None, str(exc))
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
statuses = {

View file

@ -174,39 +174,30 @@ class LndRestWallet(Wallet):
timeout=None,
)
r.raise_for_status()
except Exception as exc:
logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.")
return PaymentResponse(
False, None, None, None, f"Unable to connect to {self.endpoint}."
)
try:
data = r.json()
if data.get("payment_error"):
error_message = r.json().get("payment_error") or r.text
logger.warning(
f"LndRestWallet pay_invoice payment_error: {error_message}."
)
return PaymentResponse(False, None, None, None, error_message)
if (
"payment_hash" not in data
or "payment_route" not in data
or "total_fees_msat" not in data["payment_route"]
or "payment_preimage" not in data
):
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
)
payment_error = data.get("payment_error")
if payment_error:
logger.warning(f"LndRestWallet payment_error: {payment_error}.")
return PaymentResponse(False, None, None, None, payment_error)
checking_id = base64.b64decode(data["payment_hash"]).hex()
fee_msat = int(data["payment_route"]["total_fees_msat"])
preimage = base64.b64decode(data["payment_preimage"]).hex()
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
except KeyError as exc:
logger.warning(exc)
return PaymentResponse(
None, None, None, None, "Server error: 'missing required fields'"
)
except json.JSONDecodeError:
return PaymentResponse(
False, None, None, None, "Server error: 'invalid json response'"
None, None, None, None, "Server error: 'invalid json response'"
)
except Exception as exc:
logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.")
return PaymentResponse(
None, None, None, None, f"Unable to connect to {self.endpoint}."
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:

View file

@ -144,11 +144,11 @@ class PhoenixdWallet(Wallet):
data = r.json()
if "routingFeeSat" not in data and "reason" in data:
return PaymentResponse(False, None, None, None, data["reason"])
return PaymentResponse(None, None, None, None, data["reason"])
if r.is_error or "paymentHash" not in data:
error_message = data["message"] if "message" in data else r.text
return PaymentResponse(False, None, None, None, error_message)
return PaymentResponse(None, None, None, None, error_message)
checking_id = data["paymentHash"]
fee_msat = -int(data["routingFeeSat"])
@ -158,17 +158,17 @@ class PhoenixdWallet(Wallet):
except json.JSONDecodeError:
return PaymentResponse(
False, None, None, None, "Server error: 'invalid json response'"
None, None, None, None, "Server error: 'invalid json response'"
)
except KeyError:
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
None, None, None, None, "Server error: 'missing required fields'"
)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
return PaymentResponse(
False, None, None, None, f"Unable to connect to {self.endpoint}."
None, None, None, None, f"Unable to connect to {self.endpoint}."
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
@ -189,6 +189,7 @@ class PhoenixdWallet(Wallet):
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
# TODO: how can we detect a failed payment?
try:
r = await self.client.get(f"/payments/outgoing/{checking_id}")
if r.is_error:

View file

@ -1310,6 +1310,58 @@
}
}
},
{
"description": "failed",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"success": false,
"pending": false,
"failed": true,
"checking_id": null,
"fee_msat": null,
"preimage": null
},
"mocks": {
"corelightningrest": {
"pay_invoice_endpoint": [
{
"request_type": "data",
"request_body": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"maxfeepercent": "119.04761905",
"exemptfee": 0
},
"response_type": "json",
"response": {
"status": "failed"
}
}
]
},
"lndrest": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"payment_request": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit": 25000
},
"response_type": "json",
"response": {
"payment_error": "Test Error"
}
}
]
},
"alby": {},
"eclair": [],
"lnbits": [],
"phoenixd": []
}
},
{
"description": "pending, no fee",
"call_params": {
@ -1462,8 +1514,8 @@
},
"expect": {
"success": false,
"pending": false,
"failed": true,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null,
@ -1481,25 +1533,14 @@
},
"response_type": "json",
"response": {
"status": "pending",
"error": "Test Error"
}
}
]
},
"lndrest": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"payment_request": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit": 25000
},
"response_type": "json",
"response": {
"payment_error": "Test Error"
}
}
]
"pay_invoice_endpoint": []
},
"alby": {
"pay_invoice_endpoint": []
@ -1530,31 +1571,7 @@
]
},
"lnbits": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"out": true,
"blt11": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
},
"response_type": "json",
"response": {
"detail": "Test Error"
}
}
],
"get_payment_status_endpoint": [
{
"response_type": "json",
"response": {
"paid": true,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"details": {
"fee": 50
}
}
}
]
"pay_invoice_endpoint": []
},
"phoenixd": {
"pay_invoice_endpoint": [
@ -1591,8 +1608,8 @@
"expect": {
"error_message": "Server error: 'missing required fields'",
"success": false,
"pending": false,
"failed": true,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null
@ -1688,8 +1705,8 @@
"expect": {
"error_message": "Server error: 'invalid json response'",
"success": false,
"pending": false,
"failed": true,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null
@ -1806,8 +1823,8 @@
"expect": {
"error_message": "Unable to connect to http://127.0.0.1:8555.",
"success": false,
"pending": false,
"failed": true,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null
@ -1936,8 +1953,8 @@
"expect": {
"error_message": "Unable to connect to http://127.0.0.1:8555.",
"success": false,
"pending": false,
"failed": true,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null
@ -2643,7 +2660,17 @@
]
},
"lnbits": {
"get_payment_status_endpoint": []
"get_payment_status_endpoint": [
{
"response_type": "json",
"response": {
"paid": false,
"status": "failed",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"details": {}
}
}
]
},
"phoenixd": {
"description": "phoenixd.py doesn't handle the 'failed' status for `get_invoice_status`",

View file

@ -818,7 +818,7 @@
}
},
{
"description": "error",
"description": "failed",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
@ -826,31 +826,17 @@
"expect": {
"__eval__:error_message": "\"Payment failed: \" in \"{error_message}\"",
"success": false,
"pending": false,
"failed": true,
"checking_id": null,
"fee_msat": null,
"preimage": null
},
"mocks": {
"breez": {
"sdk_services": [
{
"response_type": "data",
"response": {
"send_payment": {
"request_type": "function",
"response_type": "exception",
"response": {
"data": "test-error"
}
}
}
}
]
},
"breez": {},
"corelightning": {
"ln": [
{
"description": "test-error",
"response": {
"call": {
"description": "indirect call to `pay` (via `call`)",
@ -867,7 +853,20 @@
},
"response_type": "exception",
"response": {
"data": "test-error"
"module": "pyln.client.lightning",
"class": "RpcError",
"data": {
"method": "test_method",
"payload": "y",
"error": {
"code": 205,
"attempts": [
{
"fail_reason": "some reason"
}
]
}
}
}
}
}
@ -994,7 +993,77 @@
}
}
}
},
}
]
}
}
},
{
"description": "error",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"__eval__:error_message": "\"Payment failed: \" in \"{error_message}\"",
"success": false,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null
},
"mocks": {
"breez": {
"sdk_services": [
{
"response_type": "data",
"response": {
"send_payment": {
"request_type": "function",
"response_type": "exception",
"response": {
"data": "test-error"
}
}
}
}
]
},
"corelightning": {
"ln": [
{
"description": "test-error",
"response": {
"call": {
"description": "indirect call to `pay` (via `call`)",
"request_type": "function",
"request_data": {
"args": [
"pay",
{
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Unit Test Invoice",
"maxfee": 25000
}
]
},
"response_type": "exception",
"response": {
"data": "test-error"
}
}
}
}
]
},
"lndrpc": {
"rpc": [
{
"response": {}
}
],
"routerpc": [
{
"description": "RPC error.",
"response": {
@ -1024,11 +1093,13 @@
"fee_limit_msat": 25000
},
"expect": {
"error_message": "Server error: 'missing required fields'",
"success": false,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null,
"error_message": "Server error: 'missing required fields'"
"preimage": null
},
"mocks": {
"breez": {
@ -1071,11 +1142,13 @@
"fee_limit_msat": 25000
},
"expect": {
"error_message": "RPC 'test_method' failed with 'test-error'.",
"success": false,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null,
"error_message": "RPC 'test_method' failed with 'test-error'."
"preimage": null
},
"mocks": {
"breez": {
@ -1083,40 +1156,6 @@
},
"corelightning": {
"ln": [
{
"response": {
"call": {
"description": "indirect call to `pay` (via `call`)",
"request_type": "function",
"request_data": {
"args": [
"pay",
{
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Unit Test Invoice",
"maxfee": 25000
}
]
},
"response_type": "exception",
"response": {
"module": "pyln.client.lightning",
"class": "RpcError",
"data": {
"method": "test_method",
"payload": "y",
"error": {
"attempts": [
{
"fail_reason": "RPC 'test_method' failed with 'test-error'."
}
]
}
}
}
}
}
},
{
"response": {
"call": {