This commit is contained in:
Djuri Baars 2026-08-10 06:51:27 -07:00 committed by GitHub
commit cd41d8f537
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 122 additions and 0 deletions

View file

@ -65,6 +65,7 @@ async def pay_invoice(
tag: str = "",
labels: list[str] | None = None,
external_id: str | None = None,
webhook: str | None = None,
conn: Connection | None = None,
) -> Payment:
if settings.lnbits_only_allow_incoming_payments:
@ -99,6 +100,7 @@ async def pay_invoice(
extra=extra,
labels=labels,
external_id=external_id,
webhook=webhook,
)
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:

View file

@ -256,6 +256,8 @@ async def api_all_payments_paginated(
`amount`, `unit`, and `memo`. To pay an arbitrary invoice from the funds
already in the authorized account, specify `out: true` and use the `bolt11`
field to supply the BOLT11 invoice to be paid.
The optional `webhook` field is honoured in both directions: LNbits POSTs
the payment to that URL once the payment reaches `success`.
""",
status_code=HTTPStatus.CREATED,
responses={
@ -281,6 +283,7 @@ async def api_payments_create(
extra=invoice_data.extra,
labels=invoice_data.labels,
external_id=invoice_data.external_id,
webhook=invoice_data.webhook,
)
return payment

View file

@ -341,6 +341,38 @@ async def test_pay_invoice(
# assert payment.payment_hash == invoice["payment_hash"]
# check POST /api/v1/payments: the webhook is kept on an outgoing payment
@pytest.mark.anyio
async def test_pay_invoice_with_webhook(
client,
inkey_headers_to,
adminkey_headers_from,
mocker: MockerFixture,
):
mocker.patch(
"lnbits.core.services.notifications.dispatch_webhook",
AsyncMock(return_value=None),
)
invoice_data = await get_random_invoice_data()
create_response = await client.post(
"/api/v1/payments", json=invoice_data, headers=inkey_headers_to
)
assert create_response.status_code < 300
webhook_url = "http://test.404.lnbits.com"
data = {
"out": True,
"bolt11": create_response.json()["bolt11"],
"webhook": webhook_url,
}
response = await client.post(
"/api/v1/payments", json=data, headers=adminkey_headers_from
)
assert response.status_code < 300
assert response.json()["webhook"] == webhook_url
# check GET /api/v1/payments/<hash>: payment status
@pytest.mark.anyio
async def test_check_payment_without_key(client, invoice: Payment):

View file

@ -533,6 +533,91 @@ async def test_pay_external_invoice_success(
assert ws_notification.call_count == 1, "Websocket notification sent."
@pytest.mark.anyio
async def test_pay_external_invoice_with_webhook(
from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet
):
invoice_amount = 2105
external_invoice = await external_funding_source.create_invoice(invoice_amount)
assert external_invoice.payment_request
assert external_invoice.checking_id
preimage = "0000000000000000000000000000000000000000000000000000000000002105"
mocker.patch(
"lnbits.wallets.FakeWallet.pay_invoice",
AsyncMock(
return_value=PaymentResponse(
ok=True, checking_id=external_invoice.checking_id, preimage=preimage
)
),
)
dispatch_webhook_mock = mocker.patch(
"lnbits.core.services.notifications.dispatch_webhook",
AsyncMock(return_value=None),
)
webhook_url = "http://test.404.lnbits.com"
payment = await pay_invoice(
wallet_id=from_wallet.id,
payment_request=external_invoice.payment_request,
webhook=webhook_url,
)
_payment = await get_standalone_payment(payment.payment_hash)
assert _payment
assert _payment.status == PaymentState.SUCCESS.value
assert _payment.amount == -invoice_amount * 1000
assert _payment.webhook == webhook_url, "Webhook is stored on the outgoing payment."
await asyncio.sleep(1)
assert (
dispatch_webhook_mock.call_count == 1
), "Webhook dispatched for the outgoing payment."
dispatched_payment = dispatch_webhook_mock.call_args_list[0][0][0]
assert dispatched_payment.payment_hash == payment.payment_hash
assert dispatched_payment.amount < 0, "Dispatched payment is the outgoing one."
assert dispatched_payment.webhook == webhook_url
@pytest.mark.anyio
async def test_pay_external_invoice_without_webhook_does_not_dispatch(
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"
mocker.patch(
"lnbits.wallets.FakeWallet.pay_invoice",
AsyncMock(
return_value=PaymentResponse(
ok=True, checking_id=external_invoice.checking_id, preimage=preimage
)
),
)
dispatch_webhook_mock = mocker.patch(
"lnbits.core.services.notifications.dispatch_webhook",
AsyncMock(return_value=None),
)
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.webhook is None
await asyncio.sleep(1)
assert dispatch_webhook_mock.call_count == 0, "No webhook, no dispatch."
@pytest.mark.anyio
async def test_retry_pay_success(
from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet