diff --git a/lnbits/core/services/payments.py b/lnbits/core/services/payments.py index 7d32799c7..9466d122e 100644 --- a/lnbits/core/services/payments.py +++ b/lnbits/core/services/payments.py @@ -29,6 +29,7 @@ from lnbits.wallets.base import ( PaymentResponse, PaymentStatus, PaymentSuccessStatus, + UnsupportedError, ) from ..crud import ( @@ -965,17 +966,15 @@ async def create_hold_invoice( invoice_memo = None if description_hash else memo funding_source = get_funding_source() - if funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"]: - raise InvoiceError( - "Hold invoices are only supported with LND.", status="failed" + try: + res = await funding_source.create_hold_invoice( + amount=amount, + memo=invoice_memo, + rhash=rhash, + description_hash=description_hash, ) - - res = await funding_source.create_hold_invoice( - amount=amount, - memo=invoice_memo, - rhash=rhash, - description_hash=description_hash, - ) + except UnsupportedError as exc: + raise InvoiceError(str(exc), status="failed") from exc if not res.ok: raise InvoiceError( @@ -1004,23 +1003,24 @@ async def create_hold_invoice( ) +# TODO: should return payment +# TODO: update payment status to success async def settle_hold_invoice( *, preimage: str, ) -> bool: - if len(preimage) != 32: + if len(bytes.fromhex(preimage)) != 32: raise InvoiceError( "Invalid preimage length. Must be 32 bytes", status="failed", ) funding_source = get_funding_source() - if funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"]: - raise InvoiceError( - "Hold invoices are only supported with LND.", status="failed" - ) - response = await funding_source.settle_hold_invoice(preimage=preimage) + try: + response = await funding_source.settle_hold_invoice(preimage=preimage) + except UnsupportedError as exc: + raise InvoiceError(str(exc), status="failed") from exc if not response.ok: raise InvoiceError("Unexpected backend error.", status="failed") @@ -1028,32 +1028,38 @@ async def settle_hold_invoice( return True -async def cancel_hold_invoice(payment_hash: str) -> bool: +async def cancel_hold_invoice(payment_hash: str) -> Payment: + payment = await get_standalone_payment(payment_hash, incoming=True) + if not payment: + raise InvoiceError("Payment not found.", status="failed") + funding_source = get_funding_source() - if funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"]: - raise InvoiceError( - "Hold invoices are only supported with LND.", status="failed" - ) - response = await funding_source.cancel_hold_invoice(payment_hash=payment_hash) + try: + response = await funding_source.cancel_hold_invoice(payment_hash=payment_hash) + except UnsupportedError as exc: + raise InvoiceError(str(exc), status="failed") from exc if not response.ok: - raise InvoiceError("Unexpected backend error.", status="failed") - - return True + raise InvoiceError( + response.error_message or "Unexpected backend error.", status="failed" + ) + payment.status = PaymentState.FAILED + await update_payment(payment) + return payment async def subscribe_hold_invoice(payment_hash: str) -> bool: payment = await get_standalone_payment(payment_hash, incoming=True) if not payment: raise InvoiceError("Payment not found.", status="failed") - funding_source = get_funding_source() - if funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"]: - raise InvoiceError( - "Hold invoices are only supported with LND.", status="failed" - ) - # if payment.webhook: - # asyncio. create_task( - # funding_source.hold_invoices_stream( - # payment_hash=payment_hash, webhook=payment.webhook - # ) - # ) + # funding_source = get_funding_source() + try: + # if payment.webhook: + # asyncio. create_task( + # funding_source.hold_invoices_stream( + # payment_hash=payment_hash, webhook=payment.webhook + # ) + # ) + pass + except UnsupportedError as exc: + raise InvoiceError(str(exc), status="failed") from exc return True diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 207da9992..c05b57c73 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -346,9 +346,12 @@ class LndRestWallet(Wallet): else: data["memo"] = memo or "" - r = await self.client.post(url="/v2/invoices/hodl", json=data) - r.raise_for_status() - data = r.json() + try: + r = await self.client.post(url="/v2/invoices/hodl", json=data) + r.raise_for_status() + data = r.json() + except httpx.HTTPStatusError as exc: + return InvoiceResponse(False, None, None, exc.response.text) payment_request = data["payment_request"] payment_hash = base64.b64encode(bytes.fromhex(rhash)).decode("ascii") @@ -357,20 +360,27 @@ class LndRestWallet(Wallet): return InvoiceResponse(True, checking_id, payment_request, None) async def settle_hold_invoice(self, preimage: str) -> PaymentResponse: - data: dict = {"preimage": base64.b64encode(preimage.encode()).decode("ascii")} - r = await self.client.post(url="/v2/invoices/settle", json=data) - r.raise_for_status() - - return PaymentResponse(True, None, None, None, None) + data: dict = { + "preimage": base64.b64encode(bytes.fromhex(preimage)).decode("ascii") + } + try: + r = await self.client.post(url="/v2/invoices/settle", json=data) + r.raise_for_status() + return PaymentResponse(True, None, None, None, None) + except httpx.HTTPStatusError as exc: + return PaymentResponse(False, None, None, None, exc.response.text) async def cancel_hold_invoice(self, payment_hash: str) -> PaymentResponse: - data: dict = { - "payment_hash": base64.b64encode(payment_hash.encode()).decode("ascii") - } - r = await self.client.post(url="/v2/invoices/cancel", json=data) - r.raise_for_status() - - return PaymentResponse(True, None, None, None, None) + rhash = bytes.fromhex(payment_hash) + try: + r = await self.client.post( + url="/v2/invoices/cancel", + json={"payment_hash": base64.b64encode(rhash).decode("ascii")}, + ) + r.raise_for_status() + return PaymentResponse(True, None, None, None, None) + except httpx.HTTPStatusError as exc: + return PaymentResponse(False, None, None, None, exc.response.text) async def hold_invoices_stream(self, payment_hash: str, webhook: str): try: diff --git a/tests/regtest/test_real_hold_invoice.py b/tests/regtest/test_real_hold_invoice.py new file mode 100644 index 000000000..6bd6e5894 --- /dev/null +++ b/tests/regtest/test_real_hold_invoice.py @@ -0,0 +1,59 @@ +import hashlib +import os + +import pytest + +from lnbits.core.services.payments import ( + cancel_hold_invoice, + create_hold_invoice, + settle_hold_invoice, +) +from lnbits.exceptions import InvoiceError + +from ..helpers import funding_source, is_fake + + +@pytest.mark.anyio +@pytest.mark.skipif(is_fake, reason="this only works in regtest") +@pytest.mark.skipif( + funding_source.__class__.__name__ in ["LndRestWallet", "LndWallet"], + reason="this should not raise for lnd", +) +async def test_pay_raise_unsupported(): + rhash = "0" * 32 + with pytest.raises(InvoiceError): + await create_hold_invoice( + wallet_id="fake_wallet_id", + amount=1000, + memo="fake_holdinvoice", + rhash=rhash, + ) + with pytest.raises(InvoiceError): + await settle_hold_invoice(preimage=rhash) + with pytest.raises(InvoiceError): + await cancel_hold_invoice(rhash) + + +@pytest.mark.anyio +@pytest.mark.skipif(is_fake, reason="this only works in regtest") +@pytest.mark.skipif( + funding_source.__class__.__name__ not in ["LndRestWallet"], + reason="this only works for lndrest", +) +async def test_pay_real_hold_invoice(from_wallet): + + preimage = os.urandom(32) + preimage_hash = hashlib.sha256(preimage).hexdigest() + payment = await create_hold_invoice( + wallet_id=from_wallet.id, + amount=1000, + memo="test_holdinvoice", + rhash=preimage_hash, + ) + assert payment.amount == 1000 * 1000 + assert payment.memo == "test_holdinvoice" + assert payment.status == "pending" + assert payment.wallet_id == from_wallet.id + + payment = await cancel_hold_invoice(payment_hash=preimage_hash) + assert payment.status == "failed"