fix: accept null optional params in make_invoice and list_transactions #1

Merged
padreug merged 2 commits from fix/null-optional-params into main 2026-09-14 20:35:06 +00:00
3 changed files with 87 additions and 11 deletions

View file

@ -5,7 +5,7 @@
"short_description": "A NWC service provider for LNbits", "short_description": "A NWC service provider for LNbits",
"description": "", "description": "",
"tile": "/nwcprovider/static/image/nwcprovider.png", "tile": "/nwcprovider/static/image/nwcprovider.png",
"version": "1.1.3", "version": "1.1.3-aio.1",
"min_lnbits_version": "1.4.0", "min_lnbits_version": "1.4.0",
"contributors": [ "contributors": [
{ {

View file

@ -264,9 +264,11 @@ async def _on_make_invoice(
# Ensures amount is provided # Ensures amount is provided
if not amount_msats: if not amount_msats:
raise Exception("Missing amount") raise Exception("Missing amount")
description = params.get("description", "") # Optional params may arrive as explicit JSON null, which dict.get does
description_hash = params.get("description_hash", None) # not default, so coerce here rather than trusting the fallback.
expiry = params.get("expiry", None) description = params.get("description") or ""
description_hash = params.get("description_hash") or None
expiry = params.get("expiry") or None
# hardening # # hardening #
assert_valid_msats(amount_msats) assert_valid_msats(amount_msats)
@ -284,7 +286,7 @@ async def _on_make_invoice(
currency="sat", currency="sat",
memo=description, memo=description,
description_hash=bytes.fromhex(description_hash) if description_hash else None, description_hash=bytes.fromhex(description_hash) if description_hash else None,
unhashed_description=description.encode("utf-8"), unhashed_description=description.encode("utf-8") if description else None,
expiry=expiry, expiry=expiry,
) )
payment_hash = payment.payment_hash payment_hash = payment.payment_hash
@ -394,12 +396,12 @@ async def _on_list_transactions(
if not nwc: if not nwc:
raise Exception("Pubkey has no associated wallet") raise Exception("Pubkey has no associated wallet")
params = payload.get("params", 0) params = payload.get("params", 0)
tfrom = params.get("from", 0) tfrom = params.get("from") or 0
tuntil = params.get("until", int(time.time())) tuntil = params.get("until") or int(time.time())
limit = params.get("limit", 10) limit = params.get("limit") or 10
offset = params.get("offset", 0) offset = params.get("offset") or 0
unpaid = params.get("unpaid", False) unpaid = params.get("unpaid") or False
tx_type = params.get("type", "") tx_type = params.get("type") or ""
# hardening # # hardening #
assert_valid_positive_int(tfrom) assert_valid_positive_int(tfrom)

View file

@ -78,3 +78,77 @@ async def test_process_invoice_backs_off_pending_payment_polling_to_configured_m
assert result["preimage"] == "b" * 64 assert result["preimage"] == "b" * 64
assert result["fee_msats"] == 10 assert result["fee_msats"] == 10
assert result["paid"] is True assert result["paid"] is True
def _stub_make_invoice(monkeypatch, captured: dict):
async def fake_get_nwc(*args, **kwargs):
return SimpleNamespace(wallet="wallet123")
async def fake_check(nwc, method):
return None
async def fake_create_invoice(**kwargs):
captured.update(kwargs)
return SimpleNamespace(payment_hash="b" * 64, bolt11="lnbc1example")
async def fake_check_transaction_status(wallet_id: str, payment_hash: str):
return SimpleNamespace(preimage=None)
monkeypatch.setattr(tasks, "get_nwc", fake_get_nwc)
monkeypatch.setattr(tasks, "_check", fake_check)
monkeypatch.setattr(tasks, "create_invoice", fake_create_invoice)
monkeypatch.setattr(
tasks, "check_transaction_status", fake_check_transaction_status
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"params",
[
# rust-nostr, Alby JS SDK: unused optional fields are omitted
{"amount": 21000},
# Amethyst: unused optional fields are sent as explicit null
{
"amount": 21000,
"description": None,
"description_hash": None,
"expiry": None,
},
],
)
async def test_make_invoice_accepts_absent_or_null_optional_params(monkeypatch, params):
captured: dict = {}
_stub_make_invoice(monkeypatch, captured)
[(result, error, _)] = await tasks._on_make_invoice(
SimpleNamespace(), "a" * 64, {"params": params}
)
assert error is None
assert result["invoice"] == "lnbc1example"
assert result["description"] == ""
assert "expires_at" not in result
assert captured["memo"] == ""
assert captured["description_hash"] is None
assert captured["unhashed_description"] is None
assert captured["expiry"] is None
@pytest.mark.asyncio
async def test_make_invoice_passes_description_and_expiry_through(monkeypatch):
captured: dict = {}
_stub_make_invoice(monkeypatch, captured)
[(result, error, _)] = await tasks._on_make_invoice(
SimpleNamespace(),
"a" * 64,
{"params": {"amount": 21000, "description": "coffee", "expiry": 600}},
)
assert error is None
assert captured["memo"] == "coffee"
assert captured["unhashed_description"] == b"coffee"
assert captured["expiry"] == 600
assert result["description"] == "coffee"
assert "expires_at" in result