workaround for xpay-handle-pay bug: use xpay if cln version supports it

This commit is contained in:
daywalker90 2025-04-07 23:50:58 +02:00
parent 7e02035d24
commit 35f8dd9c73
No known key found for this signature in database
4 changed files with 189 additions and 70 deletions

View file

@ -2,7 +2,7 @@ use std::{path::Path, time::Duration};
use cln_plugin::Plugin;
use cln_rpc::{
model::requests::{DecodeRequest, PayRequest},
model::requests::{DecodeRequest, PayRequest, XpayRequest},
primitives::Amount,
ClnRpc,
};
@ -11,7 +11,7 @@ use tokio::time;
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_invoice(
@ -110,67 +110,73 @@ pub async fn pay_invoice(
},
)?;
match rpc
.call_typed(&PayRequest {
amount_msat: params.amount.map(Amount::from_msat),
description: None,
exemptfee: None,
label: None,
localinvreqid: None,
maxdelay: None,
maxfee: None,
maxfeepercent: None,
partial_msat: None,
retry_for: None,
riskfactor: None,
exclude: None,
bolt11: params.invoice,
})
.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(),
},
id.clone(),
)
})?;
}
let my_version = plugin.state().config.lock().clone().my_cln_version;
let preimage = hex::encode(o.payment_preimage.to_vec());
Ok((nip47::PayInvoiceResponse { preimage }, id))
}
Err(e) => match e.code {
Some(c) => match c {
201 | 207 | 219 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: e.to_string(),
},
id,
)),
203 | 205 | 209 | 210 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::PaymentFailed,
message: e.to_string(),
},
id,
)),
206 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::InsufficientBalance,
message: e.to_string(),
},
id,
)),
_ => Err((
if at_or_above_version(&my_version, "24.11").map_err(|e| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id.clone(),
)
})? {
match rpc
.call_typed(&XpayRequest {
amount_msat: params.amount.map(Amount::from_msat),
maxdelay: None,
maxfee: None,
partial_msat: None,
retry_for: None,
layers: None,
invstring: params.invoice,
})
.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(),
},
id.clone(),
)
})?;
}
let preimage = hex::encode(o.payment_preimage.to_vec());
Ok((nip47::PayInvoiceResponse { preimage }, id))
}
Err(e) => match e.code {
Some(c) => match c {
207 | 219 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: e.to_string(),
},
id,
)),
203 | 205 | 209 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::PaymentFailed,
message: e.to_string(),
},
id,
)),
_ => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id,
)),
},
None => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
@ -178,14 +184,85 @@ pub async fn pay_invoice(
id,
)),
},
None => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
}
} else {
match rpc
.call_typed(&PayRequest {
amount_msat: params.amount.map(Amount::from_msat),
description: None,
exemptfee: None,
label: None,
localinvreqid: None,
maxdelay: None,
maxfee: None,
maxfeepercent: None,
partial_msat: None,
retry_for: None,
riskfactor: None,
exclude: None,
bolt11: params.invoice,
})
.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(),
},
id.clone(),
)
})?;
}
let preimage = hex::encode(o.payment_preimage.to_vec());
Ok((nip47::PayInvoiceResponse { preimage }, id))
}
Err(e) => match e.code {
Some(c) => match c {
201 | 207 | 219 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: e.to_string(),
},
id,
)),
203 | 205 | 209 | 210 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::PaymentFailed,
message: e.to_string(),
},
id,
)),
206 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::InsufficientBalance,
message: e.to_string(),
},
id,
)),
_ => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id,
)),
},
id,
)),
},
None => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id,
)),
},
}
}
}

View file

@ -1,5 +1,8 @@
use std::path::Path;
use anyhow::anyhow;
use cln_plugin::ConfiguredPlugin;
use cln_rpc::{model::requests::GetinfoRequest, ClnRpc};
use crate::{
structs::{PluginState, TimeUnit},
@ -25,7 +28,14 @@ pub async fn read_startup_options(
OPT_RELAYS.name()
));
};
let mut rpc = ClnRpc::new(
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
)
.await?;
let version = rpc.call_typed(&GetinfoRequest {}).await?.version;
let mut config = state.config.lock();
config.my_cln_version = version;
for relay in relays_str.into_iter() {
log::debug!("RELAY:{}", relay);
config.relays.push(nostr_sdk::RelayUrl::parse(&relay)?);

View file

@ -28,10 +28,14 @@ impl PluginState {
#[derive(Clone, Debug)]
pub struct Config {
pub relays: Vec<nostr_sdk::RelayUrl>,
pub my_cln_version: String,
}
impl Config {
pub fn default() -> Config {
Config { relays: Vec::new() }
Config {
relays: Vec::new(),
my_cln_version: String::new(),
}
}
}

View file

@ -79,6 +79,34 @@ pub async fn update_nwc_store(
Ok(())
}
pub fn at_or_above_version(my_version: &str, min_version: &str) -> Result<bool, anyhow::Error> {
let clean_start_my_version = my_version
.split_once('v')
.ok_or_else(|| anyhow!("Could not find v in version string"))?
.1;
let full_clean_my_version: String = clean_start_my_version
.chars()
.take_while(|x| x.is_ascii_digit() || *x == '.')
.collect();
let my_version_parts: Vec<&str> = full_clean_my_version.split('.').collect();
let min_version_parts: Vec<&str> = min_version.split('.').collect();
if my_version_parts.len() <= 1 || my_version_parts.len() > 3 {
return Err(anyhow!("Version string parse error: {}", my_version));
}
for (my, min) in my_version_parts.iter().zip(min_version_parts.iter()) {
let my_num: u32 = my.parse()?;
let min_num: u32 = min.parse()?;
if my_num != min_num {
return Ok(my_num > min_num);
}
}
Ok(my_version_parts.len() >= min_version_parts.len())
}
#[test]
fn test_budget_check() {
assert!(budget_amount_check(Some(1), Some(1), Some(2)).is_ok());