import asyncio from unittest.mock import AsyncMock import pytest from bolt11 import TagChar from bolt11 import decode as bolt11_decode from bolt11 import encode as bolt11_encode from bolt11.types import MilliSatoshi from pytest_mock.plugin import MockerFixture from lnbits.core.crud import get_standalone_payment, get_wallet from lnbits.core.models import Payment, PaymentState, Wallet from lnbits.core.services import create_invoice, pay_invoice from lnbits.exceptions import PaymentError from lnbits.settings import Settings from lnbits.tasks import ( create_permanent_task, internal_invoice_listener, register_invoice_listener, ) from lnbits.wallets.base import PaymentResponse from lnbits.wallets.fake import FakeWallet @pytest.mark.anyio async def test_invalid_bolt11(to_wallet: Wallet): with pytest.raises(PaymentError): await pay_invoice( wallet_id=to_wallet.id, payment_request="lnbcr1123123n", ) @pytest.mark.anyio async def test_amountless_invoice(to_wallet: Wallet): zero_amount_invoice = ( "lnbc1pnsu5z3pp57getmdaxhg5kc9yh2a2qsh7cjf4gnccgkw0qenm8vsqv50w7s" "ygqdqj0fjhymeqv9kk7atwwscqzzsxqyz5vqsp5e2yyqcp0a3ujeesp24ya0glej" "srh703md8mrx0g2lyvjxy5w27ss9qxpqysgqyjreasng8a086kpkczv48er5c6l5" "73aym6ynrdl9nkzqnag49vt3sjjn8qdfq5cr6ha0vrdz5c5r3v4aghndly0hplmv" "6hjxepwp93cq398l3s" ) with pytest.raises(PaymentError, match="Amountless invoices not supported."): await pay_invoice( wallet_id=to_wallet.id, payment_request=zero_amount_invoice, ) @pytest.mark.anyio async def test_bad_wallet_id(to_wallet: Wallet): payment = await create_invoice(wallet_id=to_wallet.id, amount=31, memo="Bad Wallet") bad_wallet_id = to_wallet.id[::-1] with pytest.raises( PaymentError, match=f"Could not fetch wallet '{bad_wallet_id}'." ): await pay_invoice( wallet_id=bad_wallet_id, payment_request=payment.bolt11, ) @pytest.mark.anyio async def test_payment_limit(to_wallet: Wallet): payment = await create_invoice(wallet_id=to_wallet.id, amount=101, memo="") with pytest.raises(PaymentError, match="Amount in invoice is too high."): await pay_invoice( wallet_id=to_wallet.id, max_sat=100, payment_request=payment.bolt11, ) @pytest.mark.anyio async def test_pay_twice(to_wallet: Wallet): payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Twice") await pay_invoice( wallet_id=to_wallet.id, payment_request=payment.bolt11, ) with pytest.raises(PaymentError, match="Internal invoice already paid."): await pay_invoice( wallet_id=to_wallet.id, payment_request=payment.bolt11, ) @pytest.mark.anyio async def test_fake_wallet_pay_external( to_wallet: Wallet, external_funding_source: FakeWallet ): external_invoice = await external_funding_source.create_invoice(21) assert external_invoice.payment_request with pytest.raises( PaymentError, match="Payment failed: Only internal invoices can be used!" ): await pay_invoice( wallet_id=to_wallet.id, payment_request=external_invoice.payment_request, ) @pytest.mark.anyio async def test_invoice_changed(to_wallet: Wallet): payment = await create_invoice(wallet_id=to_wallet.id, amount=21, memo="original") invoice = bolt11_decode(payment.bolt11) invoice.amount_msat = MilliSatoshi(12000) payment_request = bolt11_encode(invoice) with pytest.raises(PaymentError, match="Invalid invoice. Bolt11 changed."): await pay_invoice( wallet_id=to_wallet.id, payment_request=payment_request, ) invoice = bolt11_decode(payment_request) invoice.tags.add(TagChar.description, "mock stuff") payment_request = bolt11_encode(invoice) with pytest.raises(PaymentError, match="Invalid invoice."): await pay_invoice( wallet_id=to_wallet.id, payment_request=payment_request, ) @pytest.mark.anyio async def test_pay_for_extension(to_wallet: Wallet, settings: Settings): payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Allowed") await pay_invoice( wallet_id=to_wallet.id, payment_request=payment.bolt11, tag="lnurlp" ) payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Not Allowed") settings.lnbits_admin_extensions = ["lnurlp"] with pytest.raises( PaymentError, match="User not authorized for extension 'lnurlp'." ): await pay_invoice( wallet_id=to_wallet.id, payment_request=payment.bolt11, tag="lnurlp", ) @pytest.mark.anyio async def test_notification_for_internal_payment(to_wallet: Wallet): test_name = "test_notification_for_internal_payment" create_permanent_task(internal_invoice_listener) invoice_queue: asyncio.Queue = asyncio.Queue() register_invoice_listener(invoice_queue, test_name) payment = await create_invoice(wallet_id=to_wallet.id, amount=123, memo=test_name) await pay_invoice( wallet_id=to_wallet.id, payment_request=payment.bolt11, extra={"tag": "lnurlp"} ) await asyncio.sleep(1) while True: _payment: Payment = invoice_queue.get_nowait() # raises if queue empty assert _payment if _payment.memo == test_name: assert _payment.status == PaymentState.SUCCESS.value assert _payment.bolt11 == payment.bolt11 assert _payment.amount == 123_000 break # we found our payment, success @pytest.mark.anyio async def test_pay_failed( to_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): payment_reponse_failed = PaymentResponse(ok=False, error_message="Mock failure!") mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_failed), ) external_invoice = await external_funding_source.create_invoice(2101) assert external_invoice.payment_request assert external_invoice.checking_id with pytest.raises(PaymentError, match="Payment failed: Mock failure!"): await pay_invoice( wallet_id=to_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.amount == -2101_000 @pytest.mark.anyio async def test_retry_failed_invoice( from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): payment_reponse_failed = PaymentResponse(ok=False, error_message="Mock failure!") invoice_amount = 2102 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request ws_notification = mocker.patch( "lnbits.core.services.payments.send_payment_notification", AsyncMock(return_value=None), ) wallet = await get_wallet(from_wallet.id) assert wallet balance_before = wallet.balance with pytest.raises(PaymentError, match="Payment failed: Mock failure!"): mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_failed), ) await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) with pytest.raises( PaymentError, match="Payment is failed node, retrying is not possible." ): mocker.patch( "lnbits.wallets.FakeWallet.get_payment_status", AsyncMock(return_value=payment_reponse_failed), ) await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) wallet = await get_wallet(from_wallet.id) assert wallet assert ( balance_before == wallet.balance ), "Failed payments should not affect the balance." with pytest.raises( PaymentError, match="Failed payment was already paid on the fundingsource." ): payment_reponse_success = PaymentResponse(ok=True, error_message=None) mocker.patch( "lnbits.wallets.FakeWallet.get_payment_status", AsyncMock(return_value=payment_reponse_success), ) await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) wallet = await get_wallet(from_wallet.id) assert wallet # TODO: revisit # assert ( # balance_before - invoice_amount == wallet.balance # ), "Payment successful on retry." assert ws_notification.call_count == 0, "Websocket notification not sent." @pytest.mark.anyio async def test_pay_external_invoice_pending( from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): invoice_amount = 2103 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request assert external_invoice.checking_id preimage = "0000000000000000000000000000000000000000000000000000000000002103" payment_reponse_pending = PaymentResponse( ok=None, checking_id=external_invoice.checking_id, preimage=preimage ) mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_pending), ) ws_notification = mocker.patch( "lnbits.core.services.payments.send_payment_notification", AsyncMock(return_value=None), ) wallet = await get_wallet(from_wallet.id) assert wallet balance_before = wallet.balance payment = await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) _payment = await get_standalone_payment(payment.payment_hash) assert _payment assert _payment.status == PaymentState.PENDING.value assert _payment.checking_id == payment.payment_hash assert _payment.amount == -2103_000 assert _payment.bolt11 == external_invoice.payment_request assert _payment.preimage == preimage wallet = await get_wallet(from_wallet.id) assert wallet assert ( balance_before - invoice_amount == wallet.balance ), "Pending payment is subtracted." assert ws_notification.call_count == 0, "Websocket notification not sent." @pytest.mark.anyio async def test_retry_pay_external_invoice_pending( from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): invoice_amount = 2106 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request assert external_invoice.checking_id preimage = "0000000000000000000000000000000000000000000000000000000000002106" payment_reponse_pending = PaymentResponse( ok=None, checking_id=external_invoice.checking_id, preimage=preimage ) mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_pending), ) ws_notification = mocker.patch( "lnbits.core.services.payments.send_payment_notification", AsyncMock(return_value=None), ) wallet = await get_wallet(from_wallet.id) assert wallet balance_before = wallet.balance await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) assert ws_notification.call_count == 0, "Websocket notification not sent." with pytest.raises(PaymentError, match="Payment is still pending."): await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) wallet = await get_wallet(from_wallet.id) assert wallet # TODO: is this correct? assert ( balance_before - invoice_amount == wallet.balance ), "Failed payment is subtracted." assert ws_notification.call_count == 0, "Websocket notification not sent." @pytest.mark.anyio async def test_pay_external_invoice_success( from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): invoice_amount = 2104 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request assert external_invoice.checking_id preimage = "0000000000000000000000000000000000000000000000000000000000002104" payment_reponse_pending = PaymentResponse( ok=True, checking_id=external_invoice.checking_id, preimage=preimage ) mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_pending), ) ws_notification = mocker.patch( "lnbits.core.services.payments.send_payment_notification", AsyncMock(return_value=None), ) wallet = await get_wallet(from_wallet.id) assert wallet balance_before = wallet.balance payment = await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) _payment = await get_standalone_payment(payment.payment_hash) assert _payment assert _payment.status == PaymentState.SUCCESS.value assert _payment.checking_id == payment.payment_hash assert _payment.amount == -2104_000 assert _payment.bolt11 == external_invoice.payment_request assert _payment.preimage == preimage wallet = await get_wallet(from_wallet.id) assert wallet assert ( balance_before - invoice_amount == wallet.balance ), "Success payment is subtracted." assert ws_notification.call_count == 1, "Websocket notification sent." @pytest.mark.anyio async def test_retry_pay_success( from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): invoice_amount = 2107 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request assert external_invoice.checking_id preimage = "0000000000000000000000000000000000000000000000000000000000002107" payment_reponse_pending = PaymentResponse( ok=True, checking_id=external_invoice.checking_id, preimage=preimage ) mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_pending), ) ws_notification = mocker.patch( "lnbits.core.services.payments.send_payment_notification", AsyncMock(return_value=None), ) wallet = await get_wallet(from_wallet.id) assert wallet balance_before = wallet.balance await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) assert ws_notification.call_count == 1, "Websocket notification sent." with pytest.raises(PaymentError, match="Payment already paid."): await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) wallet = await get_wallet(from_wallet.id) assert wallet assert ( balance_before - invoice_amount == wallet.balance ), "Only one successful payment is subtracted." assert ws_notification.call_count == 1, "No new websocket notification sent." @pytest.mark.anyio async def test_pay_external_invoice_success_bad_checking_id( from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): invoice_amount = 2108 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request assert external_invoice.checking_id bad_checking_id = f"bad_{external_invoice.checking_id}" preimage = "0000000000000000000000000000000000000000000000000000000000002108" payment_reponse_success = PaymentResponse( ok=True, checking_id=bad_checking_id, preimage=preimage ) mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_success), ) 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 @pytest.mark.anyio async def test_no_checking_id( from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet ): invoice_amount = 2110 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request assert external_invoice.checking_id preimage = "0000000000000000000000000000000000000000000000000000000000002110" payment_reponse_pending = PaymentResponse( ok=True, checking_id=None, preimage=preimage ) mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_pending), ) 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.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 async def test_service_fee( from_wallet: Wallet, to_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet, settings: Settings, ): invoice_amount = 2112 external_invoice = await external_funding_source.create_invoice(invoice_amount) assert external_invoice.payment_request assert external_invoice.checking_id preimage = "0000000000000000000000000000000000000000000000000000000000002112" payment_reponse_success = PaymentResponse( ok=True, checking_id=external_invoice.checking_id, preimage=preimage ) mocker.patch( "lnbits.wallets.FakeWallet.pay_invoice", AsyncMock(return_value=payment_reponse_success), ) settings.lnbits_service_fee_wallet = to_wallet.id settings.lnbits_service_fee = 20 payment = await pay_invoice( wallet_id=from_wallet.id, payment_request=external_invoice.payment_request, ) _payment = await get_standalone_payment(payment.payment_hash) assert _payment assert _payment.status == PaymentState.SUCCESS.value assert _payment.checking_id == payment.payment_hash assert _payment.amount == -2112_000 assert _payment.fee == -422_400 assert _payment.bolt11 == external_invoice.payment_request assert _payment.preimage == preimage service_fee_payment = await get_standalone_payment( f"service_fee_{payment.payment_hash}", ) assert service_fee_payment assert service_fee_payment.status == PaymentState.SUCCESS.value assert service_fee_payment.checking_id == f"service_fee_{payment.payment_hash}" assert service_fee_payment.amount == 422_400 assert service_fee_payment.bolt11 == external_invoice.payment_request assert service_fee_payment.preimage is None