From ae4e00967d7449b92fb926015e89daf4b3cbfdba Mon Sep 17 00:00:00 2001 From: daywalker90 Date: Sun, 9 Aug 2026 17:23:19 +0200 Subject: [PATCH] fix paying of amount-less invoices with a request amount --- src/nwc_pay.rs | 78 +++++++++++++++++++++++------------------ tests/test_cln-nip47.py | 15 ++++++++ 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/nwc_pay.rs b/src/nwc_pay.rs index 9ff8635..429e0a7 100644 --- a/src/nwc_pay.rs +++ b/src/nwc_pay.rs @@ -63,16 +63,42 @@ async fn pay_invoice( let id = get_payment_id(¶ms, &decoded_invoice)?; - let invoice_amt_msat = get_invoice_amount_msat(&decoded_invoice, &id)?; + let invoice_amt_msat = get_invoice_amount_msat(&decoded_invoice); let nwc_store = load_nwc_and_check_budget(&mut rpc, label, ¶ms, invoice_amt_msat, &id).await?; + let amt_msat = match (params.amount, invoice_amt_msat) { + (None, None) => { + return Err(( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: "No amount found in request or invoice".to_owned(), + }, + Some(id.clone()), + )); + } + (None, Some(b)) => b, + (Some(a), None) => a, + (Some(a), Some(b)) => { + if a != b { + return Err(( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: "request amount does not match invoice amount".to_owned(), + }, + Some(id.clone()), + )); + } + a + } + }; + // Reserve the invoice amount plus the worst case fee so that no // combination of concurrent payments can exceed the budget and so that // balance queries during the payment reflect the reserved amount. if get_budget_msat(&nwc_store).unwrap_or(u64::MAX) - < invoice_amt_msat.saturating_add(payment_fee_reserve_msat(invoice_amt_msat)) + < amt_msat.saturating_add(payment_fee_reserve_msat(amt_msat)) { return Err(( nip47::NIP47Error { @@ -83,7 +109,7 @@ async fn pay_invoice( )); } - let reservation = reserve_budget(&mut rpc, label, &nwc_store, invoice_amt_msat) + let reservation = reserve_budget(&mut rpc, label, &nwc_store, amt_msat) .await .map_err(|e| { ( @@ -215,30 +241,15 @@ fn get_payment_id( Ok(id) } -fn get_invoice_amount_msat( - decoded_invoice: &DecodeResponse, - id: &str, -) -> Result)> { - decoded_invoice - .amount_msat - .as_ref() - .ok_or_else(|| { - ( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: "Missing amount_msat in decoded invoice".to_owned(), - }, - Some(id.to_owned()), - ) - }) - .map(Amount::msat) +fn get_invoice_amount_msat(decoded_invoice: &DecodeResponse) -> Option { + decoded_invoice.amount_msat.as_ref().map(Amount::msat) } async fn load_nwc_and_check_budget( rpc: &mut ClnRpc, label: &str, params: &nip47::PayInvoiceRequest, - invoice_amt_msat: u64, + invoice_amt_msat: Option, id: &str, ) -> Result)> { let nwc_store = load_nwc_store(rpc, label).await.map_err(|e| { @@ -251,20 +262,17 @@ async fn load_nwc_and_check_budget( ) })?; - budget_amount_check( - params.amount, - Some(invoice_amt_msat), - get_budget_msat(&nwc_store), - ) - .map_err(|e| { - ( - nip47::NIP47Error { - code: nip47::ErrorCode::QuotaExceeded, - message: e.to_string(), - }, - Some(id.to_owned()), - ) - })?; + budget_amount_check(params.amount, invoice_amt_msat, get_budget_msat(&nwc_store)).map_err( + |e| { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::QuotaExceeded, + message: e.to_string(), + }, + Some(id.to_owned()), + ) + }, + )?; Ok(nwc_store) } diff --git a/tests/test_cln-nip47.py b/tests/test_cln-nip47.py index b463ad6..36260fd 100644 --- a/tests/test_cln-nip47.py +++ b/tests/test_cln-nip47.py @@ -984,6 +984,21 @@ async def test_pay_invoice(nostr_relay, node_factory, get_plugin): # noqa: F811 PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) ) + invoice = l2.rpc.call( + "invoice", + { + "label": generate_random_label(), + "description": "test1", + "amount_msat": "any", + }, + ) + result = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=2, invoice=invoice["bolt11"]) + ) + pay = l1.rpc.call("listpays", {"payment_hash": invoice["payment_hash"]})["pays"][0] + assert result.preimage == pay["preimage"] + assert pay["amount_msat"] == 2 + @pytest.mark.asyncio async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811