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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-07 23:50:58 +02:00
|
|
|
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}"));
|
2025-04-07 23:50:58 +02:00
|
|
|
}
|
|
|
|
|
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());
|
|
|
|
|
}
|