cln-nip47/src/util.rs

210 lines
6.8 KiB
Rust
Raw Normal View History

2025-04-02 17:38:52 +02:00
use anyhow::anyhow;
2025-11-09 16:02:15 +01:00
use cln_plugin::Plugin;
2025-04-02 17:38:52 +02:00
use cln_rpc::{
model::requests::{DatastoreMode, DatastoreRequest, ListdatastoreRequest},
ClnRpc,
};
2025-11-09 16:02:15 +01:00
use nostr_sdk::nips::nip47;
use crate::{
structs::{NwcStore, PluginState},
2025-11-10 15:36:56 +01:00
OPT_NOTIFICATIONS,
PLUGIN_NAME,
2025-11-08 13:41:11 +01:00
WALLET_HOLD_METHODS,
WALLET_HOLD_NOTIFICATIONS,
2025-11-10 15:36:56 +01:00
WALLET_NOTIFICATIONS,
WALLET_PAY_METHODS,
WALLET_READ_METHODS,
2025-11-09 16:02:15 +01:00
};
2025-04-02 17:38:52 +02:00
pub fn budget_amount_check(
request_amt_msat: Option<u64>,
invoice_amt_msat: Option<u64>,
budget_msat: Option<u64>,
) -> Result<(), anyhow::Error> {
log::debug!(
2025-11-06 15:12:52 +01:00
"checking budget and amounts for request:{request_amt_msat:?} \
invoice:{invoice_amt_msat:?} budget:{budget_msat:?}"
2025-04-02 17:38:52 +02:00
);
if request_amt_msat.is_none() && invoice_amt_msat.is_none() {
return Err(anyhow!("No amount given to check budget against!"));
}
if let Some(req_amt) = request_amt_msat {
if let Some(inv_amt) = invoice_amt_msat {
if req_amt != inv_amt {
return Err(anyhow!("Amount from request and invoice differ!"));
}
}
}
if let Some(bdgt_msat) = budget_msat {
if let Some(req_amt) = request_amt_msat {
if bdgt_msat < req_amt {
return Err(anyhow!("Payment exceeds budget!"));
}
}
if let Some(inv_amt) = invoice_amt_msat {
if bdgt_msat < inv_amt {
return Err(anyhow!("Payment exceeds budget!"));
}
}
}
Ok(())
}
2025-11-09 15:15:10 +01:00
pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &str) -> Result<NwcStore, anyhow::Error> {
2025-04-02 17:38:52 +02:00
let nwc_store_store = rpc
.call_typed(&ListdatastoreRequest {
2025-11-09 15:15:10 +01:00
key: Some(vec![PLUGIN_NAME.to_owned(), label.to_owned()]),
2025-04-02 17:38:52 +02:00
})
.await?
.datastore;
let nwc_store_str = nwc_store_store
.first()
2025-11-06 15:12:52 +01:00
.ok_or_else(|| anyhow!("No datastore found for: {label}"))?
2025-04-02 17:38:52 +02:00
.string
.as_ref()
.ok_or_else(|| anyhow!("Malformed nwc_store datastore: missing string"))?;
let nwc_store: NwcStore = serde_json::from_str(nwc_store_str)?;
2025-11-06 15:12:52 +01:00
log::debug!("loaded nwc store for label:{label}");
2025-04-02 17:38:52 +02:00
Ok(nwc_store)
}
pub async fn update_nwc_store(
rpc: &mut ClnRpc,
2025-11-09 15:15:10 +01:00
label: &str,
2025-04-02 17:38:52 +02:00
nwc_store: NwcStore,
) -> Result<(), anyhow::Error> {
rpc.call_typed(&DatastoreRequest {
2025-11-09 15:15:10 +01:00
key: vec![PLUGIN_NAME.to_owned(), label.to_owned()],
2025-04-02 17:38:52 +02:00
generation: None,
hex: None,
mode: Some(DatastoreMode::CREATE_OR_REPLACE),
string: Some(serde_json::to_string(&nwc_store)?),
})
.await?;
2025-11-06 15:12:52 +01:00
log::debug!("stored nwc store for label:{label}");
2025-04-02 17:38:52 +02:00
Ok(())
}
2025-05-04 11:42:29 +02:00
pub fn is_read_only_nwc(nwc_store: &NwcStore) -> bool {
if let Some(budget_msat) = nwc_store.budget_msat {
if budget_msat == 0 && nwc_store.interval_config.is_none() {
return true;
}
}
false
}
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 {
2025-11-06 15:12:52 +01:00
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())
}
2025-11-09 16:02:15 +01:00
pub fn build_capabilities(is_read_only: bool, plugin: &Plugin<PluginState>) -> (String, String) {
2025-11-08 13:41:11 +01:00
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
2025-11-09 16:02:15 +01:00
let mut methods = WALLET_READ_METHODS.map(|m| m.to_string()).join(" ");
if !is_read_only {
methods.push(' ');
methods.push_str(WALLET_PAY_METHODS.map(|m| m.to_string()).join(" ").as_str());
}
2025-11-08 13:41:11 +01:00
if holdinvoice_support {
methods.push(' ');
methods.push_str(
WALLET_HOLD_METHODS
.map(|m| m.to_string())
.join(" ")
.as_str(),
);
}
2025-11-09 16:02:15 +01:00
let mut notifications = String::new();
if plugin.option(&OPT_NOTIFICATIONS).unwrap() {
notifications.push_str(
WALLET_NOTIFICATIONS
.map(|m| m.to_string())
.join(" ")
.as_str(),
);
2025-11-08 13:41:11 +01:00
if holdinvoice_support {
notifications.push(' ');
notifications.push_str(
WALLET_HOLD_NOTIFICATIONS
.map(|m| m.to_string())
.join(" ")
.as_str(),
);
}
2025-11-09 16:02:15 +01:00
}
(methods, notifications)
}
2025-11-08 13:41:11 +01:00
pub fn build_methods_vec(is_read_only: bool, plugin: &Plugin<PluginState>) -> Vec<nip47::Method> {
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
2025-11-09 16:02:15 +01:00
let mut methods = WALLET_READ_METHODS.to_vec();
if !is_read_only {
methods.extend_from_slice(&WALLET_PAY_METHODS);
}
2025-11-08 13:41:11 +01:00
if holdinvoice_support {
methods.extend_from_slice(&WALLET_HOLD_METHODS);
}
2025-11-09 16:02:15 +01:00
methods
}
pub fn build_notifications_vec(plugin: &Plugin<PluginState>) -> Vec<String> {
2025-11-08 13:41:11 +01:00
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
2025-11-09 16:02:15 +01:00
let mut notifications = Vec::new();
if plugin.option(&OPT_NOTIFICATIONS).unwrap() {
notifications.extend_from_slice(&WALLET_NOTIFICATIONS.map(|m| m.to_string()));
2025-11-08 13:41:11 +01:00
if holdinvoice_support {
notifications.extend_from_slice(&WALLET_HOLD_NOTIFICATIONS.map(|m| m.to_string()));
}
2025-11-09 16:02:15 +01:00
}
notifications
}
2025-04-02 17:38:52 +02:00
#[test]
fn test_budget_check() {
assert!(budget_amount_check(Some(1), Some(1), Some(2)).is_ok());
assert!(budget_amount_check(Some(1), Some(2), Some(2)).is_err());
assert!(budget_amount_check(Some(2), Some(2), Some(1)).is_err());
assert!(budget_amount_check(Some(2), None, None).is_ok());
assert!(budget_amount_check(Some(2), None, Some(2)).is_ok());
assert!(budget_amount_check(None, None, None).is_err());
assert!(budget_amount_check(None, None, Some(2)).is_err());
assert!(budget_amount_check(Some(0), None, Some(1)).is_ok());
assert!(budget_amount_check(Some(0), None, Some(0)).is_ok());
assert!(budget_amount_check(None, Some(0), Some(1)).is_ok());
assert!(budget_amount_check(None, Some(0), Some(0)).is_ok());
assert!(budget_amount_check(Some(0), Some(0), Some(1)).is_ok());
assert!(budget_amount_check(Some(0), Some(0), Some(0)).is_ok());
}