diff --git a/lnbits/core/services/payments.py b/lnbits/core/services/payments.py index f52029b66..434784d1d 100644 --- a/lnbits/core/services/payments.py +++ b/lnbits/core/services/payments.py @@ -20,7 +20,7 @@ from lnbits.exceptions import InvoiceError, PaymentError from lnbits.fiat import get_fiat_provider from lnbits.helpers import check_callback_url from lnbits.settings import settings -from lnbits.tasks import internal_invoice_queue_put +from lnbits.tasks import create_task, internal_invoice_queue_put from lnbits.utils.crypto import fake_privkey, random_secret_and_hash from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat from lnbits.wallets import fake_wallet, get_funding_source @@ -711,8 +711,6 @@ async def _pay_external_invoice( fee_reserve_msat = fee_reserve(amount_msat, internal=False) service_fee_msat = service_fee(amount_msat, internal=False) - from lnbits.tasks import create_task - task = create_task( _fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat) ) @@ -726,41 +724,28 @@ async def _pay_external_invoice( logger.debug(f"payment timeout, {checking_id} is still pending") return payment - if payment_response.checking_id and payment_response.checking_id != checking_id: - logger.warning( - f"backend sent unexpected checking_id (expected: {checking_id} got:" - f" {payment_response.checking_id})" - ) - if payment_response.checking_id and payment_response.ok is not False: - # payment.ok can be True (paid) or None (pending)! - logger.debug(f"updating payment {checking_id}") - payment.status = ( - PaymentState.SUCCESS - if payment_response.ok is True - else PaymentState.PENDING - ) - payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat)) - payment.preimage = payment_response.preimage - await update_payment(payment, payment_response.checking_id, conn=conn) - payment.checking_id = payment_response.checking_id - if payment.success: - await send_payment_notification(wallet, payment) - logger.success(f"payment successful {payment_response.checking_id}") - elif payment_response.checking_id is None and payment_response.ok is False: - # payment failed - logger.debug(f"payment failed {checking_id}, {payment_response.error_message}") + # payment failed + if ( + payment_response.checking_id is None + or payment_response.ok is False + or payment_response.checking_id != checking_id + ): payment.status = PaymentState.FAILED await update_payment(payment, conn=conn) - raise PaymentError( - f"Payment failed: {payment_response.error_message}" - or "Payment failed, but backend didn't give us an error message.", - status="failed", - ) - else: - logger.warning( - "didn't receive checking_id from backend, payment may be stuck in" - f" database: {checking_id}" - ) + message = payment_response.error_message or "without an error message." + raise PaymentError(f"Payment failed: {message}", status="failed") + + # payment.ok can be True (paid) or None (pending)! + payment.status = ( + PaymentState.SUCCESS if payment_response.ok is True else PaymentState.PENDING + ) + payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat)) + payment.preimage = payment_response.preimage + await update_payment(payment, payment_response.checking_id, conn=conn) + payment.checking_id = payment_response.checking_id + if payment.success: + await send_payment_notification(wallet, payment) + logger.success(f"payment successful {payment_response.checking_id}") return payment diff --git a/lnbits/core/views/payment_api.py b/lnbits/core/views/payment_api.py index 62c4bcd5b..6962597cf 100644 --- a/lnbits/core/views/payment_api.py +++ b/lnbits/core/views/payment_api.py @@ -378,6 +378,9 @@ async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)): return {"paid": True, "preimage": payment.preimage, "details": payment} return {"paid": True, "preimage": payment.preimage} + if payment.failed: + return {"paid": False, "status": "failed", "details": payment} + try: status = await payment.check_status() except Exception: diff --git a/lnbits/wallets/corelightningrest.py b/lnbits/wallets/corelightningrest.py index 386cb1ffb..842927d7a 100644 --- a/lnbits/wallets/corelightningrest.py +++ b/lnbits/wallets/corelightningrest.py @@ -218,7 +218,7 @@ class CoreLightningRestWallet(Wallet): data = exc.response.json() error_code = int(data["error"]["code"]) if error_code in self.pay_failure_error_codes: - error_message = f"Payment failed: {data['error']['message']}" + error_message = data["error"]["message"] return PaymentResponse(ok=False, error_message=error_message) error_message = f"REST failed with {data['error']['message']}." return PaymentResponse(error_message=error_message) diff --git a/tests/regtest/conftest.py b/tests/regtest/conftest.py index cea47f680..51b00c7c2 100644 --- a/tests/regtest/conftest.py +++ b/tests/regtest/conftest.py @@ -1,6 +1,6 @@ import pytest -from .helpers import get_hold_invoice, get_real_invoice +from .helpers import get_hold_invoice, get_real_invoice, get_real_invoice_noroute @pytest.fixture(scope="function") @@ -22,3 +22,13 @@ async def real_amountless_invoice(): invoice = get_real_invoice(0) yield invoice["payment_request"] del invoice + + +@pytest.fixture(scope="function") +async def real_invoice_noroute(): + invoice = get_real_invoice_noroute(100) + yield { + "bolt11": invoice["payment_request"], + "payment_hash": invoice["r_hash"], + } + del invoice diff --git a/tests/regtest/helpers.py b/tests/regtest/helpers.py index da8d47e6a..b8e7c07fa 100644 --- a/tests/regtest/helpers.py +++ b/tests/regtest/helpers.py @@ -39,6 +39,17 @@ docker_lightning_unconnected_cli = [ ] +docker_lightning_noroute_cli = [ + "docker", + "exec", + "lnbits-lnd-4-1", + "lncli", + "--network", + "regtest", + "--rpcserver=lnd-4", +] + + def run_cmd(cmd: list) -> str: timeout = 10 process = Popen(cmd, stdout=PIPE, stderr=PIPE) @@ -100,6 +111,12 @@ def get_real_invoice(sats: int) -> dict: return run_cmd_json(cmd) +def get_real_invoice_noroute(sats: int) -> dict: + cmd = docker_lightning_noroute_cli.copy() + cmd.extend(["addinvoice", str(sats)]) + return run_cmd_json(cmd) + + def pay_real_invoice(invoice: str) -> str: cmd = docker_lightning_cli.copy() cmd.extend(["payinvoice", "--force", invoice]) diff --git a/tests/regtest/test_real_invoice.py b/tests/regtest/test_real_invoice.py index 99319abbe..ff937b404 100644 --- a/tests/regtest/test_real_invoice.py +++ b/tests/regtest/test_real_invoice.py @@ -69,6 +69,68 @@ async def test_pay_real_invoice( assert prev_balance - balance == 100 +@pytest.mark.anyio +@pytest.mark.skipif(is_fake, reason="this only works in regtest") +async def test_pay_real_invoice_noroute( + client, + real_invoice_noroute, + adminkey_headers_from, + inkey_headers_from, +): + response = await client.post( + "/api/v1/payments", json=real_invoice_noroute, headers=adminkey_headers_from + ) + assert response.status_code == 520 + invoice = response.json() + + assert invoice["status"] == "failed" + + # check the payment status + response = await client.get( + f'/api/v1/payments/{real_invoice_noroute["payment_hash"]}', + headers=inkey_headers_from, + ) + assert response.status_code < 300 + payment = response.json() + assert payment["paid"] is False + assert payment["status"] == "failed" + + +@pytest.mark.anyio +@pytest.mark.skipif(is_fake, reason="this only works in regtest") +async def test_pay_real_invoice_mainnet( + client, + adminkey_headers_from, + inkey_headers_from, +): + """regtest should fail paying a mainnet invoice""" + inv = ( + "lnbc100n1p59ujlrpp5mn5g5tu7fz0up6asnz0gcceru4hwz0w42g7fuz8gxw67jl7kjjeqcqzyssp5qq" + "5y92fwazqdtnu8u9p9qf333hqnkuvtuu5csdze5ak4q86hyrhq9q7sqqqqqqqqqqqqqqqqqqqsqqqqqys" + "gqdq2f38xy6t5wvmqz9gxqrrssrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc" + "lll4ttz7sp6kpvqqqqlgqqqqqeqqjqm9fkydmtwsxcxx3j44x9fckjqttg54zlxzw92yeaz9nzn8w7hgv" + "87ph5ug4wmgxqpk929k7l6dsnc2y9532daaqlpg9tfjglshuh48cpjh0dua" + ) + payment_hash = "dce88a2f9e489fc0ebb0989e8c6323e56ee13dd5523c9e08e833b5e97fd694b2" + + response = await client.post( + "/api/v1/payments", json={"bolt11": inv}, headers=adminkey_headers_from + ) + assert response.status_code == 520 + invoice = response.json() + assert invoice["status"] == "failed" + + # check the payment status + response = await client.get( + f"/api/v1/payments/{payment_hash}", + headers=inkey_headers_from, + ) + assert response.status_code < 300 + payment = response.json() + assert payment["paid"] is False + assert payment["status"] == "failed" + + @pytest.mark.anyio @pytest.mark.skipif(is_fake, reason="this only works in regtest") async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_from): diff --git a/tests/unit/test_pay_invoice.py b/tests/unit/test_pay_invoice.py index dc4e019b5..6569c4947 100644 --- a/tests/unit/test_pay_invoice.py +++ b/tests/unit/test_pay_invoice.py @@ -558,18 +558,14 @@ async def test_pay_external_invoice_success_bad_checking_id( AsyncMock(return_value=payment_reponse_success), ) - await pay_invoice( - wallet_id=from_wallet.id, - payment_request=external_invoice.payment_request, - ) + with pytest.raises(PaymentError): + await pay_invoice( + wallet_id=from_wallet.id, + payment_request=external_invoice.payment_request, + ) payment = await get_standalone_payment(bad_checking_id) - assert payment - assert payment.checking_id == bad_checking_id, "checking_id updated" - assert payment.payment_hash == external_invoice.checking_id - assert payment.amount == -2108_000 - assert payment.preimage == preimage - assert payment.status == PaymentState.SUCCESS.value + assert payment is None, "Payment should not be created with bad checking_id" @pytest.mark.anyio @@ -590,19 +586,21 @@ async def test_no_checking_id( AsyncMock(return_value=payment_reponse_pending), ) - await pay_invoice( - wallet_id=from_wallet.id, - payment_request=external_invoice.payment_request, - ) + with pytest.raises(PaymentError): + await pay_invoice( + wallet_id=from_wallet.id, + payment_request=external_invoice.payment_request, + ) payment = await get_standalone_payment(external_invoice.checking_id) assert payment + assert payment.status == PaymentState.FAILED.value + assert payment.checking_id == external_invoice.checking_id assert payment.payment_hash == external_invoice.checking_id assert payment.amount == -2110_000 assert payment.preimage is None - assert payment.status == PaymentState.PENDING.value @pytest.mark.anyio