From af306edd3a8a0b1b080a115e48100241f3278b20 Mon Sep 17 00:00:00 2001 From: daywalker90 <8257956+daywalker90@users.noreply.github.com> Date: Sun, 17 May 2026 19:46:22 +0200 Subject: [PATCH] use xkeysend on CLN v26.06+ --- CHANGELOG.md | 3 + src/nwc_keysend.rs | 204 ++++++++++++++++++++++++++++++--------------- src/nwc_pay.rs | 9 +- 3 files changed, 147 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fd46e..08b1dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - holdinvoice methods: ``make_hold_invoice``, ``cancel_hold_invoice``, ``settle_hold_invoice`` - holdinvoice notification: ``hold_invoice_accepted`` +### Changed +- `pay_keysend` will use the new CLN `xkeysend` command using CLN v26.06+ + ### Removed - ``multi_pay_invoice`` and ``multi_pay_keysend``, they were removed from the spec diff --git a/src/nwc_keysend.rs b/src/nwc_keysend.rs index bc49ec2..c5c5673 100644 --- a/src/nwc_keysend.rs +++ b/src/nwc_keysend.rs @@ -1,15 +1,15 @@ -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use cln_plugin::Plugin; use cln_rpc::{ - model::requests::KeysendRequest, + model::requests::{KeysendRequest, XkeysendRequest}, primitives::{Amount, PublicKey, TlvEntry, TlvStream}, }; use nostr::nips::nip47; use crate::{ structs::PluginState, - util::{budget_amount_check, load_nwc_store, update_nwc_store}, + util::{at_or_above_version, budget_amount_check, load_nwc_store, update_nwc_store}, }; pub async fn pay_keysend_response( @@ -76,75 +76,147 @@ async fn pay_keysend( message: e.to_string(), })?; - let mut extratlvs = TlvStream { - entries: Vec::new(), - }; - for tlv in params.tlv_records { - extratlvs.entries.push(TlvEntry { - typ: tlv.tlv_type, - value: tlv.value.as_bytes().to_owned(), - }); - } - let extratlvs = if extratlvs.entries.is_empty() { - None - } else { - Some(extratlvs) - }; + let my_cln_version = plugin.state().config.lock().my_cln_version.clone(); - match rpc - .call_typed(&KeysendRequest { - exemptfee: None, - extratlvs, - label: None, - maxdelay: None, - maxfee: None, - maxfeepercent: None, - retry_for: None, - routehints: None, - amount_msat: Amount::from_msat(params.amount), - destination: pubkey, - }) - .await - { - Ok(o) => { - if let Some(ref mut bdg) = nwc_store.budget_msat { - *bdg = bdg.saturating_sub(o.amount_sent_msat.msat()); - update_nwc_store(&mut rpc, label, nwc_store) - .await - .map_err(|e| nip47::NIP47Error { + if at_or_above_version(&my_cln_version, "26.06").map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: e.to_string(), + })? { + let mut extratlvs = HashMap::with_capacity(params.tlv_records.len()); + for tlv in params.tlv_records { + extratlvs.insert(tlv.tlv_type.to_string(), tlv.value); + } + let extratlvs = if extratlvs.is_empty() { + None + } else { + Some(extratlvs) + }; + + match rpc + .call_typed(&XkeysendRequest { + extratlvs, + label: None, + maxdelay: None, + maxfee: None, + retry_for: None, + layers: None, + amount_msat: Amount::from_msat(params.amount), + destination: pubkey, + }) + .await + { + Ok(o) => { + if let Some(ref mut bdg) = nwc_store.budget_msat { + *bdg = bdg.saturating_sub(o.amount_sent_msat.msat()); + update_nwc_store(&mut rpc, label, nwc_store) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + } + + let preimage = hex::encode(o.payment_preimage.to_vec()); + + let fees_paid = o.amount_sent_msat.msat() - o.amount_msat.msat(); + + Ok(nip47::PayKeysendResponse { + preimage, + fees_paid: Some(fees_paid), + }) + } + Err(e) => match e.code { + Some(c) => match c { + 203 | 205 | 207 | 219 => Err(nip47::NIP47Error { + code: nip47::ErrorCode::PaymentFailed, + message: e.to_string(), + }), + 209 => Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: e.to_string(), + }), + _ => Err(nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), - })?; - } - - let preimage = hex::encode(o.payment_preimage.to_vec()); - - let fees_paid = o.amount_sent_msat.msat() - o.amount_msat.msat(); - - Ok(nip47::PayKeysendResponse { - preimage, - fees_paid: Some(fees_paid), - }) - } - Err(e) => match e.code { - Some(c) => match c { - 203 | 205 | 210 => Err(nip47::NIP47Error { - code: nip47::ErrorCode::PaymentFailed, - message: e.to_string(), - }), - 206 => Err(nip47::NIP47Error { - code: nip47::ErrorCode::InsufficientBalance, - message: e.to_string(), - }), - _ => Err(nip47::NIP47Error { + }), + }, + None => Err(nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), }), }, - None => Err(nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }), - }, + } + } else { + let mut extratlvs = TlvStream { + entries: Vec::new(), + }; + for tlv in params.tlv_records { + extratlvs.entries.push(TlvEntry { + typ: tlv.tlv_type, + value: tlv.value.as_bytes().to_owned(), + }); + } + let extratlvs = if extratlvs.entries.is_empty() { + None + } else { + Some(extratlvs) + }; + + match rpc + .call_typed(&KeysendRequest { + exemptfee: None, + extratlvs, + label: None, + maxdelay: None, + maxfee: None, + maxfeepercent: None, + retry_for: None, + routehints: None, + amount_msat: Amount::from_msat(params.amount), + destination: pubkey, + }) + .await + { + Ok(o) => { + if let Some(ref mut bdg) = nwc_store.budget_msat { + *bdg = bdg.saturating_sub(o.amount_sent_msat.msat()); + update_nwc_store(&mut rpc, label, nwc_store) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + } + + let preimage = hex::encode(o.payment_preimage.to_vec()); + + let fees_paid = o.amount_sent_msat.msat() - o.amount_msat.msat(); + + Ok(nip47::PayKeysendResponse { + preimage, + fees_paid: Some(fees_paid), + }) + } + Err(e) => match e.code { + Some(c) => match c { + 203 | 205 | 210 => Err(nip47::NIP47Error { + code: nip47::ErrorCode::PaymentFailed, + message: e.to_string(), + }), + 206 => Err(nip47::NIP47Error { + code: nip47::ErrorCode::InsufficientBalance, + message: e.to_string(), + }), + _ => Err(nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + }), + }, + None => Err(nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + }), + }, + } } } diff --git a/src/nwc_pay.rs b/src/nwc_pay.rs index a313720..4f4e859 100644 --- a/src/nwc_pay.rs +++ b/src/nwc_pay.rs @@ -1,17 +1,17 @@ use cln_plugin::Plugin; use cln_rpc::{ + ClnRpc, + RpcError, model::{ requests::{DecodeRequest, PayRequest, XpayRequest}, responses::DecodeResponse, }, primitives::{Amount, Secret}, - ClnRpc, - RpcError, }; use nostr::nips::nip47; use crate::{ - structs::{NwcStore, PluginState, NOT_INV_ERR}, + structs::{NOT_INV_ERR, NwcStore, PluginState}, util::{at_or_above_version, budget_amount_check, load_nwc_store, update_nwc_store}, }; @@ -316,6 +316,9 @@ async fn pay_with_xpay_full( layers: None, invstring: params.invoice, payer_note: None, + dev_use_shadow: None, + label: None, + localinvreqid: None, }) .await .map_err(|e| map_cln_error_to_nip47(&e, id, true))?;