feat: add support for paying amountless BOLT11 invoices
Some checks are pending
LNbits CI / migration (3.12) (push) Blocked by required conditions
LNbits CI / openapi (push) Blocked by required conditions
LNbits CI / lint (push) Waiting to run
LNbits CI / test-api (, 3.10) (push) Blocked by required conditions
LNbits CI / migration (3.11) (push) Blocked by required conditions
LNbits CI / test-api (, 3.11) (push) Blocked by required conditions
LNbits CI / test-api (, 3.12) (push) Blocked by required conditions
LNbits CI / test-api (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.10) (push) Blocked by required conditions
LNbits CI / test-api (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.11) (push) Blocked by required conditions
LNbits CI / test-api (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.12) (push) Blocked by required conditions
LNbits CI / test-wallets (, 3.10) (push) Blocked by required conditions
LNbits CI / test-wallets (, 3.11) (push) Blocked by required conditions
LNbits CI / test-wallets (, 3.12) (push) Blocked by required conditions
LNbits CI / test-wallets (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.10) (push) Blocked by required conditions
LNbits CI / test-wallets (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.11) (push) Blocked by required conditions
LNbits CI / test-wallets (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.12) (push) Blocked by required conditions
LNbits CI / test-unit (, 3.10) (push) Blocked by required conditions
LNbits CI / test-unit (, 3.11) (push) Blocked by required conditions
LNbits CI / test-unit (, 3.12) (push) Blocked by required conditions
LNbits CI / test-unit (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.10) (push) Blocked by required conditions
LNbits CI / test-unit (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.11) (push) Blocked by required conditions
LNbits CI / test-unit (postgres://lnbits:lnbits@0.0.0.0:5432/lnbits, 3.12) (push) Blocked by required conditions
LNbits CI / migration (3.10) (push) Blocked by required conditions
LNbits CI / regtest (BoltzWallet, 3.10) (push) Blocked by required conditions
LNbits CI / regtest (CoreLightningRestWallet, 3.10) (push) Blocked by required conditions
LNbits CI / regtest (CoreLightningWallet, 3.10) (push) Blocked by required conditions
LNbits CI / regtest (EclairWallet, 3.10) (push) Blocked by required conditions
LNbits CI / regtest (LNbitsWallet, 3.10) (push) Blocked by required conditions
LNbits CI / regtest (LndRestWallet, 3.10) (push) Blocked by required conditions
LNbits CI / regtest (LndWallet, 3.10) (push) Blocked by required conditions
LNbits CI / jmeter (3.10) (push) Blocked by required conditions
codeql / analyze (push) Waiting to run

Add ability to pay BOLT11 invoices that don't have an embedded amount
by specifying the amount at payment time via the `amount_msat` parameter.

Changes:
- Add `Feature.amountless_invoice` to wallet base class for capability detection
- Update `Wallet.pay_invoice()` signature with optional `amount_msat` parameter
- Implement amountless invoice support in LND REST and LND gRPC wallets
- Update payment service layer to validate and pass through amount_msat
- Add `amount_msat` field to CreateInvoice API model
- Update all wallet implementations with new method signature
- Add tests for amountless invoice payment flow

Usage: POST /api/v1/payments with `amount_msat` when paying amountless invoices

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-01-09 19:12:42 +01:00 committed by padreug
parent 359e9d8b29
commit 8657e221c6
29 changed files with 697 additions and 45 deletions

View file

@ -997,3 +997,71 @@ async def _create_some_payments(payment_count: int, client, payments_headers):
data = response.json()
assert data["labels"] == labels
return payment_count
################################ Amountless Invoices ################################
@pytest.mark.anyio
async def test_pay_amountless_invoice_with_amount(client, adminkey_headers_from):
"""Test paying an amountless invoice by specifying amount_msat.
This tests the primary use case: receiving an amountless invoice from an
external source and paying it with a specified amount.
"""
from lnbits.wallets import get_funding_source
# Create an amountless invoice directly using FakeWallet
# (bypassing the service layer which blocks amountless invoices for creation)
funding_source = get_funding_source()
invoice_response = await funding_source.create_invoice(
amount=0, memo="test_amountless_external"
)
assert invoice_response.ok
assert invoice_response.payment_request
# Verify it's amountless
decoded = bolt11.decode(invoice_response.payment_request)
assert decoded.amount_msat is None
# Pay the amountless invoice with an explicit amount via API
pay_data = {
"out": True,
"bolt11": invoice_response.payment_request,
"amount_msat": 5000, # 5 sats in msat
}
response = await client.post(
"/api/v1/payments", json=pay_data, headers=adminkey_headers_from
)
assert response.status_code < 300
payment = response.json()
assert "payment_hash" in payment
assert payment["payment_hash"] == invoice_response.checking_id
@pytest.mark.anyio
async def test_pay_amountless_invoice_without_amount_fails(
client, adminkey_headers_from
):
"""Test that paying an amountless invoice without amount_msat fails."""
from lnbits.wallets import get_funding_source
# Create an amountless invoice directly using FakeWallet
funding_source = get_funding_source()
invoice_response = await funding_source.create_invoice(
amount=0, memo="test_fail_amountless"
)
assert invoice_response.ok
assert invoice_response.payment_request
# Try to pay without specifying amount - should fail
pay_data = {
"out": True,
"bolt11": invoice_response.payment_request,
}
response = await client.post(
"/api/v1/payments", json=pay_data, headers=adminkey_headers_from
)
# Should fail because amount is required for amountless invoices
assert response.status_code >= 400
assert "Amount required" in response.json().get("detail", "")

View file

@ -42,7 +42,7 @@ async def test_amountless_invoice(to_wallet: Wallet):
"73aym6ynrdl9nkzqnag49vt3sjjn8qdfq5cr6ha0vrdz5c5r3v4aghndly0hplmv"
"6hjxepwp93cq398l3s"
)
with pytest.raises(PaymentError, match="Amountless invoices not supported."):
with pytest.raises(PaymentError, match="Amount required for amountless invoices."):
await pay_invoice(
wallet_id=to_wallet.id,
payment_request=zero_amount_invoice,