mirror of
https://github.com/daywalker90/cln-nip47.git
synced 2026-08-13 12:33:43 +02:00
support receive-only NWC announcements
This commit is contained in:
parent
84c8ffd584
commit
97c89858eb
14 changed files with 444 additions and 258 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## [0.1.3] Unreleased
|
||||
|
||||
### Changed
|
||||
- if your NWC does not have a renewing budget set with interval and the budget is 0 (aka "receive-only" NWC) it will now also not set any of the pay methods when announcing itself to relays, this is for services which demand a receive-only NWC (e.g. stacker.news) and determine it by the announced methods
|
||||
|
||||
## [0.1.2] 2025-04-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
59
src/main.rs
59
src/main.rs
|
|
@ -14,7 +14,7 @@ use parse::read_startup_options;
|
|||
use rpc::{nwc_budget, nwc_create, nwc_list, nwc_revoke};
|
||||
use structs::PluginState;
|
||||
use tokio::time;
|
||||
use util::load_nwc_store;
|
||||
use util::{load_nwc_store, update_nwc_store};
|
||||
|
||||
mod nwc;
|
||||
mod nwc_balance;
|
||||
|
|
@ -40,6 +40,24 @@ const OPT_NOTIFICATIONS: DefaultBooleanConfigOption = ConfigOption::new_bool_wit
|
|||
"Enable/disable nip47-notifications. Default is `true`",
|
||||
);
|
||||
pub const PLUGIN_NAME: &str = "cln-nip47";
|
||||
pub const WALLET_READ_METHODS: [&str; 5] = [
|
||||
"make_invoice",
|
||||
"lookup_invoice",
|
||||
"list_transactions",
|
||||
"get_balance",
|
||||
"get_info",
|
||||
];
|
||||
pub const WALLET_ALL_METHODS: [&str; 9] = [
|
||||
"pay_invoice",
|
||||
"multi_pay_invoice",
|
||||
"pay_keysend",
|
||||
"multi_pay_keysend",
|
||||
WALLET_READ_METHODS[0],
|
||||
WALLET_READ_METHODS[1],
|
||||
WALLET_READ_METHODS[2],
|
||||
WALLET_READ_METHODS[3],
|
||||
WALLET_READ_METHODS[4],
|
||||
];
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
|
|
@ -49,7 +67,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
);
|
||||
log_panics::init();
|
||||
|
||||
let state = PluginState::default();
|
||||
let state;
|
||||
|
||||
let confplugin = match Builder::new(tokio::io::stdin(), tokio::io::stdout())
|
||||
.option(OPT_RELAYS)
|
||||
|
|
@ -66,6 +84,16 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
.await?
|
||||
{
|
||||
Some(plugin) => {
|
||||
let rpc_file = Path::new(&plugin.configuration().lightning_dir)
|
||||
.join(plugin.configuration().rpc_file);
|
||||
state = match PluginState::new(rpc_file).await {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
return plugin
|
||||
.disable(format!("Error connecting to cln rpc: {}", e).as_str())
|
||||
.await;
|
||||
}
|
||||
};
|
||||
match read_startup_options(&plugin, &state).await {
|
||||
Ok(()) => &(),
|
||||
Err(e) => return plugin.disable(format!("{}", e).as_str()).await,
|
||||
|
|
@ -78,13 +106,13 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
let plugin = confplugin.start(state).await?;
|
||||
|
||||
{
|
||||
let _guard = plugin.state().rpc_lock.lock().await;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
// Make sure incase of rapid nip47-create and plugin restarts info_events
|
||||
// have a different timestamp and therefore ID so relays don't disconnect us
|
||||
time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
match load_nwcs(plugin.clone()).await {
|
||||
match load_nwcs(plugin.clone(), &mut rpc).await {
|
||||
Ok(_) => log::info!("All NWC's loaded"),
|
||||
Err(e) => {
|
||||
println!(
|
||||
|
|
@ -112,11 +140,7 @@ async fn shutdown_handler(
|
|||
std::process::exit(0)
|
||||
}
|
||||
|
||||
async fn load_nwcs(plugin: Plugin<PluginState>) -> Result<(), anyhow::Error> {
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await?;
|
||||
async fn load_nwcs(plugin: Plugin<PluginState>, rpc: &mut ClnRpc) -> Result<(), anyhow::Error> {
|
||||
let labels = rpc
|
||||
.call_typed(&ListdatastoreRequest {
|
||||
key: Some(vec![PLUGIN_NAME.to_owned()]),
|
||||
|
|
@ -124,15 +148,18 @@ async fn load_nwcs(plugin: Plugin<PluginState>) -> Result<(), anyhow::Error> {
|
|||
.await?;
|
||||
for datastore in labels.datastore.into_iter() {
|
||||
let label = datastore.key.last().unwrap();
|
||||
let nwc_store = load_nwc_store(&mut rpc, label).await?;
|
||||
let mut nwc_store = load_nwc_store(rpc, label).await?;
|
||||
|
||||
let client = run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await?;
|
||||
// check NWC's created with cln-nip47 <= v0.1.3 for intervals with 0 reset budget
|
||||
if let Some(interval_conf) = &nwc_store.interval_config {
|
||||
if interval_conf.reset_budget_msat == 0 {
|
||||
nwc_store.interval_config = None;
|
||||
nwc_store.budget_msat = Some(0);
|
||||
update_nwc_store(rpc, label, nwc_store.clone()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut client_handles = plugin.state().handles.lock().await;
|
||||
client_handles.insert(
|
||||
label.clone(),
|
||||
(client, Keys::new(nwc_store.uri.secret).public_key()),
|
||||
);
|
||||
run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
161
src/nwc.rs
161
src/nwc.rs
|
|
@ -8,7 +8,9 @@ use crate::nwc_lookups::{list_transactions, lookup_invoice};
|
|||
use crate::nwc_pay::{multi_pay_invoice, pay_invoice};
|
||||
use crate::structs::{NwcStore, PluginState};
|
||||
use crate::tasks::budget_task;
|
||||
use crate::OPT_NOTIFICATIONS;
|
||||
use crate::util::is_read_only_nwc;
|
||||
use crate::{OPT_NOTIFICATIONS, WALLET_ALL_METHODS, WALLET_READ_METHODS};
|
||||
use anyhow::anyhow;
|
||||
use cln_plugin::Plugin;
|
||||
use nostr_sdk::nips::*;
|
||||
use nostr_sdk::Client;
|
||||
|
|
@ -20,12 +22,18 @@ pub async fn run_nwc(
|
|||
plugin: Plugin<PluginState>,
|
||||
label: String,
|
||||
nwc_store: NwcStore,
|
||||
) -> Result<client::Client, client::Error> {
|
||||
) -> Result<(), client::Error> {
|
||||
let capabilities = if is_read_only_nwc(&nwc_store) {
|
||||
WALLET_READ_METHODS.join(" ")
|
||||
} else {
|
||||
WALLET_ALL_METHODS.join(" ")
|
||||
};
|
||||
|
||||
let wallet_keys = Keys::new(
|
||||
SecretKey::from_hex(&nwc_store.walletkey)
|
||||
.map_err(|e| client::Error::Signer(SignerError::backend(e)))?,
|
||||
);
|
||||
let client_pubkey = Keys::new(nwc_store.uri.secret).public_key();
|
||||
let client_pubkey = Keys::new(nwc_store.uri.secret.clone()).public_key();
|
||||
|
||||
let client = Client::new(wallet_keys.clone());
|
||||
|
||||
|
|
@ -37,12 +45,12 @@ pub async fn run_nwc(
|
|||
}
|
||||
|
||||
if nwc_store.interval_config.is_some() {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
tokio::spawn(budget_task(rx, plugin.clone(), label.clone()));
|
||||
plugin.state().budget_jobs.lock().insert(label.clone(), tx);
|
||||
start_nwc_budget_job(plugin.clone(), label.clone());
|
||||
}
|
||||
|
||||
let client_clone = client.clone();
|
||||
let plugin_clone = plugin.clone();
|
||||
let label_clone = label.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
client_clone.connect().await;
|
||||
|
|
@ -68,46 +76,15 @@ pub async fn run_nwc(
|
|||
continue;
|
||||
}
|
||||
|
||||
let mut info_event_builder = EventBuilder::new(
|
||||
Kind::WalletConnectInfo,
|
||||
"pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_invoice \
|
||||
lookup_invoice list_transactions get_balance get_info",
|
||||
if let Err(e) = send_nwc_info_event(
|
||||
client_clone.clone(),
|
||||
plugin_clone.option(&OPT_NOTIFICATIONS).unwrap(),
|
||||
capabilities.clone(),
|
||||
wallet_keys.clone(),
|
||||
)
|
||||
.tag(Tag::parse(vec!["encryption", "nip44_v2 nip04"]).unwrap());
|
||||
|
||||
if plugin.option(&OPT_NOTIFICATIONS).unwrap() {
|
||||
info_event_builder = info_event_builder.tag(
|
||||
Tag::parse(vec!["notifications", "payment_received payment_sent"]).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
let info_event = match info_event_builder.sign_with_keys(&wallet_keys) {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
log::warn!("Could not sign info_event! {}", e);
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
log::debug!("info_event:{:?}", info_event);
|
||||
let send_result = match client_clone.send_event(&info_event).await {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
log::warn!("Could not send info_event! {}", e);
|
||||
client_clone.disconnect().await;
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if send_result.success.is_empty() {
|
||||
log::warn!(
|
||||
"None of the relays received the info_event! {}",
|
||||
send_result
|
||||
.failed
|
||||
.into_values()
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
.await
|
||||
{
|
||||
log::warn!("{}", e);
|
||||
client_clone.disconnect().await;
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
continue;
|
||||
|
|
@ -117,20 +94,19 @@ pub async fn run_nwc(
|
|||
.kind(Kind::WalletConnectRequest)
|
||||
.author(client_pubkey);
|
||||
|
||||
match client_clone.subscribe(filter, None).await {
|
||||
Ok(_o) => (),
|
||||
Err(e) => {
|
||||
log::warn!("Could not subscribe to nwc events! {}", e);
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = client_clone.subscribe(filter, None).await {
|
||||
log::warn!("Could not subscribe to nwc events! {}", e);
|
||||
client_clone.disconnect().await;
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
continue;
|
||||
};
|
||||
|
||||
let client_clone_handler = client_clone.clone();
|
||||
match client_clone
|
||||
.handle_notifications(|notification| {
|
||||
let client_clone_handler = client_clone_handler.clone();
|
||||
let plugin_clone = plugin.clone();
|
||||
let label_clone = label.clone();
|
||||
let plugin_clone = plugin_clone.clone();
|
||||
let label_clone = label_clone.clone();
|
||||
let wallet_keys_clone = wallet_keys.clone();
|
||||
nwc_request_handler(
|
||||
notification,
|
||||
|
|
@ -144,14 +120,83 @@ pub async fn run_nwc(
|
|||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
log::info!("NWC handler for `{}` stopped", label);
|
||||
log::info!("NWC handler for `{}` stopped", label_clone);
|
||||
break;
|
||||
}
|
||||
Err(e) => log::warn!("NWC handler for `{}` had an error: {}", label, e),
|
||||
Err(e) => log::warn!("NWC handler for `{}` had an error: {}", label_clone, e),
|
||||
};
|
||||
}
|
||||
});
|
||||
Ok(client)
|
||||
|
||||
let mut locked_handles = plugin.state().handles.lock().await;
|
||||
locked_handles.insert(
|
||||
label.clone(),
|
||||
(client, Keys::new(nwc_store.uri.secret).public_key()),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_nwc_info_event(
|
||||
client: Client,
|
||||
notifications: bool,
|
||||
capabilities: String,
|
||||
wallet_keys: Keys,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut info_event_builder = EventBuilder::new(Kind::WalletConnectInfo, capabilities.clone())
|
||||
.tag(Tag::parse(vec!["encryption", "nip44_v2 nip04"]).unwrap());
|
||||
|
||||
if notifications {
|
||||
info_event_builder = info_event_builder
|
||||
.tag(Tag::parse(vec!["notifications", "payment_received payment_sent"]).unwrap())
|
||||
}
|
||||
|
||||
let info_event = match info_event_builder.sign_with_keys(&wallet_keys) {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
return Err(anyhow!("Could not sign info_event! {}", e));
|
||||
}
|
||||
};
|
||||
log::debug!("info_event:{:?}", info_event);
|
||||
let send_result = match client.send_event(&info_event).await {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
return Err(anyhow!("Could not send info_event! {}", e));
|
||||
}
|
||||
};
|
||||
if send_result.success.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"None of the relays received the info_event! {}",
|
||||
send_result
|
||||
.failed
|
||||
.into_values()
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop_nwc(plugin: Plugin<PluginState>, label: &String) {
|
||||
let mut locked_handles = plugin.state().handles.lock().await;
|
||||
if let Some((client, _client_pubkey)) = locked_handles.remove(label) {
|
||||
client.shutdown().await;
|
||||
}
|
||||
|
||||
stop_nwc_budget_job(plugin.clone(), label);
|
||||
}
|
||||
|
||||
pub fn start_nwc_budget_job(plugin: Plugin<PluginState>, label: String) {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
tokio::spawn(budget_task(rx, plugin.clone(), label.clone()));
|
||||
plugin.state().budget_jobs.lock().insert(label, tx);
|
||||
}
|
||||
|
||||
pub fn stop_nwc_budget_job(plugin: Plugin<PluginState>, label: &String) {
|
||||
let mut budget_jobs = plugin.state().budget_jobs.lock();
|
||||
let job = budget_jobs.remove(label);
|
||||
if let Some(j) = job {
|
||||
let _ = j.send(());
|
||||
}
|
||||
}
|
||||
|
||||
async fn nwc_request_handler(
|
||||
|
|
@ -358,7 +403,7 @@ async fn nwc_request_handler(
|
|||
}]
|
||||
}
|
||||
nip47::RequestParams::GetInfo => {
|
||||
vec![match get_info(plugin.clone()).await {
|
||||
vec![match get_info(plugin.clone(), &label).await {
|
||||
Ok(o) => (
|
||||
nip47::Response {
|
||||
result_type: nip47::Method::GetInfo,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use std::path::Path;
|
||||
|
||||
use cln_plugin::Plugin;
|
||||
use cln_rpc::{model::requests::ListpeerchannelsRequest, primitives::ChannelState, ClnRpc};
|
||||
use cln_rpc::{model::requests::ListpeerchannelsRequest, primitives::ChannelState};
|
||||
use nostr_sdk::nips::*;
|
||||
|
||||
use crate::{structs::PluginState, util::load_nwc_store};
|
||||
|
|
@ -10,14 +8,7 @@ pub async fn get_balance(
|
|||
plugin: Plugin<PluginState>,
|
||||
label: &String,
|
||||
) -> Result<nip47::GetBalanceResponse, nip47::NIP47Error> {
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let nwc_store = load_nwc_store(&mut rpc, label)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
use std::{path::Path, str::FromStr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use cln_plugin::Plugin;
|
||||
use cln_rpc::{model::requests::GetinfoRequest, ClnRpc};
|
||||
use cln_rpc::model::requests::GetinfoRequest;
|
||||
use nostr_sdk::nips::*;
|
||||
use nostr_sdk::*;
|
||||
|
||||
use crate::structs::PluginState;
|
||||
use crate::OPT_NOTIFICATIONS;
|
||||
use crate::util::{is_read_only_nwc, load_nwc_store};
|
||||
use crate::{OPT_NOTIFICATIONS, WALLET_ALL_METHODS, WALLET_READ_METHODS};
|
||||
|
||||
pub async fn get_info(
|
||||
plugin: Plugin<PluginState>,
|
||||
label: &String,
|
||||
) -> Result<nip47::GetInfoResponse, nip47::NIP47Error> {
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let get_info = rpc
|
||||
.call_typed(&GetinfoRequest {})
|
||||
|
|
@ -46,6 +41,25 @@ pub async fn get_info(
|
|||
vec![]
|
||||
};
|
||||
|
||||
let nwc_store = load_nwc_store(&mut rpc, label)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
let methods = if is_read_only_nwc(&nwc_store) {
|
||||
WALLET_READ_METHODS
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
} else {
|
||||
WALLET_ALL_METHODS
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
};
|
||||
|
||||
Ok(nip47::GetInfoResponse {
|
||||
alias: get_info.alias,
|
||||
color: Some(get_info.color),
|
||||
|
|
@ -53,17 +67,7 @@ pub async fn get_info(
|
|||
network: Some(network),
|
||||
block_height: Some(get_info.blockheight),
|
||||
block_hash: None,
|
||||
methods: vec![
|
||||
"pay_invoice".to_owned(),
|
||||
"multi_pay_invoice".to_owned(),
|
||||
"pay_keysend".to_owned(),
|
||||
"multi_pay_keysend".to_owned(),
|
||||
"make_invoice".to_owned(),
|
||||
"lookup_invoice".to_owned(),
|
||||
"list_transactions".to_owned(),
|
||||
"get_balance".to_owned(),
|
||||
"get_info".to_owned(),
|
||||
],
|
||||
methods,
|
||||
notifications,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
use std::{path::Path, str::FromStr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use cln_plugin::Plugin;
|
||||
use cln_rpc::{
|
||||
model::requests::InvoiceRequest,
|
||||
primitives::{Amount, AmountOrAny, Sha256},
|
||||
ClnRpc,
|
||||
};
|
||||
use nostr_sdk::nips::*;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -15,14 +14,7 @@ pub async fn make_invoice(
|
|||
plugin: Plugin<PluginState>,
|
||||
params: nip47::MakeInvoiceRequest,
|
||||
) -> Result<nip47::MakeInvoiceResponse, nip47::NIP47Error> {
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let mut deschashonly = None;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
use std::{path::Path, str::FromStr, time::Duration};
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use cln_plugin::Plugin;
|
||||
use cln_rpc::{
|
||||
model::requests::KeysendRequest,
|
||||
primitives::{Amount, PublicKey, TlvEntry, TlvStream},
|
||||
ClnRpc,
|
||||
};
|
||||
use nostr_sdk::nips::*;
|
||||
use tokio::time;
|
||||
|
|
@ -19,7 +18,7 @@ pub async fn pay_keysend(
|
|||
params: nip47::PayKeysendRequest,
|
||||
label: &String,
|
||||
) -> Result<nip47::PayKeysendResponse, nip47::NIP47Error> {
|
||||
let _guard = plugin.state().rpc_lock.lock().await;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
if params.preimage.is_some() {
|
||||
return Err(nip47::NIP47Error {
|
||||
|
|
@ -28,15 +27,6 @@ pub async fn pay_keysend(
|
|||
});
|
||||
}
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut nwc_store = load_nwc_store(&mut rpc, label)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{cmp::Reverse, path::Path, str::FromStr};
|
||||
use std::{cmp::Reverse, str::FromStr};
|
||||
|
||||
use cln_plugin::Plugin;
|
||||
use cln_rpc::{
|
||||
|
|
@ -7,7 +7,6 @@ use cln_rpc::{
|
|||
responses::{ListinvoicesInvoicesStatus, ListpaysPaysStatus},
|
||||
},
|
||||
primitives::Sha256,
|
||||
ClnRpc,
|
||||
};
|
||||
use nostr_sdk::nips::*;
|
||||
use nostr_sdk::*;
|
||||
|
|
@ -18,14 +17,7 @@ pub async fn lookup_invoice(
|
|||
plugin: Plugin<PluginState>,
|
||||
params: nip47::LookupInvoiceRequest,
|
||||
) -> Result<nip47::LookupInvoiceResponse, nip47::NIP47Error> {
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
if params.payment_hash.is_none() && params.invoice.is_none() {
|
||||
return Err(nip47::NIP47Error {
|
||||
|
|
@ -252,14 +244,7 @@ pub async fn list_transactions(
|
|||
plugin: Plugin<PluginState>,
|
||||
params: nip47::ListTransactionsRequest,
|
||||
) -> Result<Vec<nip47::LookupInvoiceResponse>, nip47::NIP47Error> {
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let (query_invoices, query_payments) = match params.transaction_type {
|
||||
Some(t) => match t {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
|
@ -6,7 +5,6 @@ use cln_plugin::Plugin;
|
|||
use cln_rpc::model::requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest};
|
||||
use cln_rpc::model::responses::ListpaysPaysStatus;
|
||||
use cln_rpc::primitives::Sha256;
|
||||
use cln_rpc::ClnRpc;
|
||||
|
||||
use crate::structs::PluginState;
|
||||
use crate::OPT_NOTIFICATIONS;
|
||||
|
|
@ -29,10 +27,7 @@ pub async fn payment_received_handler(
|
|||
.as_str()
|
||||
.ok_or_else(|| anyhow!("label not a string"))?;
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await?;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let invoice_resp = rpc
|
||||
.call_typed(&ListinvoicesRequest {
|
||||
|
|
@ -196,10 +191,7 @@ pub async fn payment_sent_handler(
|
|||
.ok_or_else(|| anyhow!("payment_hash not a string"))?
|
||||
.to_owned();
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await?;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let pays_resp = rpc
|
||||
.call_typed(&ListpaysRequest {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
use std::{path::Path, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use cln_plugin::Plugin;
|
||||
use cln_rpc::{
|
||||
model::requests::{DecodeRequest, PayRequest, XpayRequest},
|
||||
primitives::Amount,
|
||||
ClnRpc,
|
||||
};
|
||||
use nostr_sdk::nips::*;
|
||||
use tokio::time;
|
||||
|
|
@ -19,24 +18,10 @@ pub async fn pay_invoice(
|
|||
params: nip47::PayInvoiceRequest,
|
||||
label: &String,
|
||||
) -> Result<(nip47::PayInvoiceResponse, String), (nip47::NIP47Error, String)> {
|
||||
let _guard = plugin.state().rpc_lock.lock().await;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let id = params.id.clone().unwrap_or_default();
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
nip47::NIP47Error {
|
||||
code: nip47::ErrorCode::Internal,
|
||||
message: e.to_string(),
|
||||
},
|
||||
id.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let invoice_decoded = rpc
|
||||
.call_typed(&DecodeRequest {
|
||||
string: params.invoice.clone(),
|
||||
|
|
|
|||
139
src/rpc.rs
139
src/rpc.rs
|
|
@ -1,28 +1,25 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use cln_plugin::Plugin;
|
||||
use cln_rpc::model::requests::{
|
||||
DatastoreMode, DatastoreRequest, DeldatastoreRequest, ListdatastoreRequest,
|
||||
};
|
||||
use cln_rpc::ClnRpc;
|
||||
use nostr_sdk::nips::nip47::*;
|
||||
use nostr_sdk::*;
|
||||
use serde_json::json;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::nwc::run_nwc;
|
||||
use crate::nwc::{
|
||||
run_nwc, send_nwc_info_event, start_nwc_budget_job, stop_nwc, stop_nwc_budget_job,
|
||||
};
|
||||
use crate::parse::parse_time_period;
|
||||
use crate::structs::{BudgetIntervalConfig, NwcStore, PluginState};
|
||||
use crate::tasks::budget_task;
|
||||
use crate::util::{load_nwc_store, update_nwc_store};
|
||||
use crate::PLUGIN_NAME;
|
||||
use crate::util::{is_read_only_nwc, load_nwc_store, update_nwc_store};
|
||||
use crate::{OPT_NOTIFICATIONS, PLUGIN_NAME, WALLET_ALL_METHODS, WALLET_READ_METHODS};
|
||||
|
||||
pub async fn nwc_create(
|
||||
plugin: Plugin<PluginState>,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, anyhow::Error> {
|
||||
let _guard = plugin.state().rpc_lock.lock().await;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let (label, budget_msat, interval_secs) = parse_full_args(args)?;
|
||||
|
||||
|
|
@ -49,11 +46,6 @@ pub async fn nwc_create(
|
|||
serde_json::Value::String(client_keys.public_key().to_string()),
|
||||
);
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let interval_config = if let Some(bgt_msat) = budget_msat {
|
||||
result.insert(
|
||||
"budget_msat".to_owned(),
|
||||
|
|
@ -89,12 +81,8 @@ pub async fn nwc_create(
|
|||
})
|
||||
.await?;
|
||||
|
||||
let client = run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await?;
|
||||
let mut locked_handles = plugin.state().handles.lock().await;
|
||||
locked_handles.insert(
|
||||
label.clone(),
|
||||
(client, Keys::new(nwc_store.uri.secret).public_key()),
|
||||
);
|
||||
run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await?;
|
||||
|
||||
Ok(serde_json::Value::Object(result))
|
||||
}
|
||||
|
||||
|
|
@ -102,27 +90,11 @@ pub async fn nwc_revoke(
|
|||
plugin: Plugin<PluginState>,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, anyhow::Error> {
|
||||
let _guard = plugin.state().rpc_lock.lock().await;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let label = parse_revoke_args(args)?;
|
||||
|
||||
{
|
||||
let mut locked_handles = plugin.state().handles.lock().await;
|
||||
if let Some((client, _client_pubkey)) = locked_handles.remove(&label) {
|
||||
client.shutdown().await;
|
||||
}
|
||||
|
||||
let mut budget_jobs = plugin.state().budget_jobs.lock();
|
||||
let job = budget_jobs.remove(&label);
|
||||
if let Some(j) = job {
|
||||
let _ = j.send(());
|
||||
}
|
||||
}
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await?;
|
||||
stop_nwc(plugin.clone(), &label).await;
|
||||
|
||||
rpc.call_typed(&DeldatastoreRequest {
|
||||
generation: None,
|
||||
|
|
@ -137,25 +109,16 @@ pub async fn nwc_budget(
|
|||
plugin: Plugin<PluginState>,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, anyhow::Error> {
|
||||
let _guard = plugin.state().rpc_lock.lock().await;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let (label, budget_msat, interval_secs) = parse_full_args(args)?;
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut budget_jobs = plugin.state().budget_jobs.lock();
|
||||
let job = budget_jobs.remove(&label);
|
||||
if let Some(j) = job {
|
||||
let _ = j.send(());
|
||||
}
|
||||
}
|
||||
stop_nwc_budget_job(plugin.clone(), &label);
|
||||
|
||||
let mut nwc_store = load_nwc_store(&mut rpc, &label).await?;
|
||||
|
||||
let is_old_nwc_read_only = is_read_only_nwc(&nwc_store);
|
||||
|
||||
if let Some(budget) = budget_msat {
|
||||
nwc_store.budget_msat = Some(budget);
|
||||
if let Some(interval) = interval_secs {
|
||||
|
|
@ -173,13 +136,34 @@ pub async fn nwc_budget(
|
|||
nwc_store.interval_config = None;
|
||||
}
|
||||
|
||||
let is_new_nwc_read_only = is_read_only_nwc(&nwc_store);
|
||||
|
||||
if nwc_store.interval_config.is_some() {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
tokio::spawn(budget_task(rx, plugin.clone(), label.clone()));
|
||||
plugin.state().budget_jobs.lock().insert(label.clone(), tx);
|
||||
start_nwc_budget_job(plugin.clone(), label.clone());
|
||||
}
|
||||
|
||||
update_nwc_store(&mut rpc, &label, nwc_store).await?;
|
||||
update_nwc_store(&mut rpc, &label, nwc_store.clone()).await?;
|
||||
|
||||
if is_old_nwc_read_only != is_new_nwc_read_only {
|
||||
let wallet_keys = Keys::new(SecretKey::from_hex(&nwc_store.walletkey)?);
|
||||
let capabilities = if is_new_nwc_read_only {
|
||||
WALLET_READ_METHODS.join(" ")
|
||||
} else {
|
||||
WALLET_ALL_METHODS.join(" ")
|
||||
};
|
||||
let clients = plugin.state().handles.lock().await;
|
||||
send_nwc_info_event(
|
||||
clients
|
||||
.get(&label)
|
||||
.ok_or_else(|| anyhow!("No client found for label: {}", label))?
|
||||
.0
|
||||
.clone(),
|
||||
plugin.option(&OPT_NOTIFICATIONS).unwrap(),
|
||||
capabilities,
|
||||
wallet_keys,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(json!({"budget_updated":label}))
|
||||
}
|
||||
|
|
@ -188,15 +172,10 @@ pub async fn nwc_list(
|
|||
plugin: Plugin<PluginState>,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, anyhow::Error> {
|
||||
let _guard = plugin.state().rpc_lock.lock().await;
|
||||
let mut rpc = plugin.state().rpc_lock.lock().await;
|
||||
|
||||
let label = parse_list_args(args)?;
|
||||
|
||||
let mut rpc = ClnRpc::new(
|
||||
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut nwcs = Vec::new();
|
||||
|
||||
if let Some(lbl) = label {
|
||||
|
|
@ -248,16 +227,20 @@ pub async fn nwc_list(
|
|||
fn parse_full_args(
|
||||
args: serde_json::Value,
|
||||
) -> Result<(String, Option<u64>, Option<u64>), anyhow::Error> {
|
||||
let label;
|
||||
let budget_msat;
|
||||
let interval_secs;
|
||||
|
||||
match args {
|
||||
serde_json::Value::String(s) => Ok((s, None, None)),
|
||||
serde_json::Value::String(s) => return Ok((s, None, None)),
|
||||
serde_json::Value::Array(values) => {
|
||||
let label = values
|
||||
label = values
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("label missing"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("label is not a string"))?
|
||||
.to_owned();
|
||||
let budget_msat = if let Some(b) = values.get(1) {
|
||||
budget_msat = if let Some(b) = values.get(1) {
|
||||
Some(
|
||||
b.as_u64()
|
||||
.ok_or_else(|| anyhow!("budget_msat is not an integer"))?,
|
||||
|
|
@ -265,7 +248,7 @@ fn parse_full_args(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let interval_secs = if let Some(t) = values.get(2) {
|
||||
interval_secs = if let Some(t) = values.get(2) {
|
||||
Some(parse_time_period(
|
||||
t.as_str()
|
||||
.ok_or_else(|| anyhow!("interval is not a string"))?,
|
||||
|
|
@ -273,19 +256,15 @@ fn parse_full_args(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
if interval_secs.is_some() && budget_msat.is_none() {
|
||||
return Err(anyhow!("Must set `budget_msat` if you use `interval`"));
|
||||
}
|
||||
Ok((label, budget_msat, interval_secs))
|
||||
}
|
||||
serde_json::Value::Object(map) => {
|
||||
let label = map
|
||||
label = map
|
||||
.get("label")
|
||||
.ok_or_else(|| anyhow!("label missing"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("label is not a string"))?
|
||||
.to_owned();
|
||||
let budget_msat = if let Some(b) = map.get("budget_msat") {
|
||||
budget_msat = if let Some(b) = map.get("budget_msat") {
|
||||
Some(
|
||||
b.as_u64()
|
||||
.ok_or_else(|| anyhow!("budget_msat is not an integer"))?,
|
||||
|
|
@ -293,7 +272,7 @@ fn parse_full_args(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let interval_secs = if let Some(t) = map.get("interval") {
|
||||
interval_secs = if let Some(t) = map.get("interval") {
|
||||
Some(parse_time_period(
|
||||
t.as_str()
|
||||
.ok_or_else(|| anyhow!("interval is not a string"))?,
|
||||
|
|
@ -301,13 +280,19 @@ fn parse_full_args(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
if interval_secs.is_some() && budget_msat.is_none() {
|
||||
return Err(anyhow!("Must set `budget_msat` if you use `interval`"));
|
||||
}
|
||||
Ok((label, budget_msat, interval_secs))
|
||||
}
|
||||
_ => Err(anyhow!("Invalid argument type")),
|
||||
_ => return Err(anyhow!("Invalid argument type")),
|
||||
}
|
||||
|
||||
if interval_secs.is_some() && budget_msat.is_none() {
|
||||
return Err(anyhow!("Must set `budget_msat` if you use `interval`"));
|
||||
}
|
||||
if interval_secs.is_some() && budget_msat.unwrap() == 0 {
|
||||
return Err(anyhow!(
|
||||
"`budget_msat` must be greater than 0 if you use `interval`"
|
||||
));
|
||||
}
|
||||
Ok((label, budget_msat, interval_secs))
|
||||
}
|
||||
|
||||
fn parse_revoke_args(args: serde_json::Value) -> Result<String, anyhow::Error> {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
||||
use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc};
|
||||
|
||||
use cln_rpc::ClnRpc;
|
||||
use nostr_sdk::client;
|
||||
use nostr_sdk::nips::nip47;
|
||||
use nostr_sdk::nostr;
|
||||
|
|
@ -11,17 +12,17 @@ use tokio::sync::oneshot;
|
|||
pub struct PluginState {
|
||||
pub config: Arc<Mutex<Config>>,
|
||||
pub handles: Arc<tokio::sync::Mutex<HashMap<String, (client::Client, nostr::PublicKey)>>>,
|
||||
pub rpc_lock: Arc<tokio::sync::Mutex<()>>,
|
||||
pub rpc_lock: Arc<tokio::sync::Mutex<ClnRpc>>,
|
||||
pub budget_jobs: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>,
|
||||
}
|
||||
impl PluginState {
|
||||
pub fn default() -> PluginState {
|
||||
PluginState {
|
||||
pub async fn new(path: PathBuf) -> Result<PluginState, anyhow::Error> {
|
||||
Ok(PluginState {
|
||||
config: Arc::new(Mutex::new(Config::default())),
|
||||
handles: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
||||
rpc_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||
rpc_lock: Arc::new(tokio::sync::Mutex::new(ClnRpc::new(path).await?)),
|
||||
budget_jobs: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@ pub async fn update_nwc_store(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -100,7 +100,8 @@ async def test_get_info(node_factory, get_plugin, nostr_client): # noqa: F811
|
|||
node_get_info = l1.rpc.call("getinfo", {})
|
||||
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
||||
LOGGER.info(uri_str)
|
||||
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
|
||||
uri = NostrWalletConnectUri.parse(uri_str)
|
||||
nwc = Nwc(uri)
|
||||
get_info = await nwc.get_info()
|
||||
assert get_info.alias == node_get_info["alias"]
|
||||
assert get_info.block_height == node_get_info["blockheight"]
|
||||
|
|
@ -149,6 +150,57 @@ async def test_get_info(node_factory, get_plugin, nostr_client): # noqa: F811
|
|||
assert get_info.notifications == []
|
||||
assert get_info.pubkey == node_get_info["id"]
|
||||
|
||||
signer = NostrSigner.keys(Keys(uri.secret()))
|
||||
client = Client(signer)
|
||||
await client.add_relay(f"ws://{url}")
|
||||
await client.connect()
|
||||
|
||||
response_filter = Filter().kind(Kind(13194)).author(uri.public_key())
|
||||
events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10))
|
||||
start_time = datetime.now()
|
||||
while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10):
|
||||
time.sleep(1)
|
||||
events = await client.fetch_events(
|
||||
response_filter, timeout=timedelta(seconds=1)
|
||||
)
|
||||
assert events.len() == 1
|
||||
assert (
|
||||
events.to_vec()[0].content()
|
||||
== "pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_invoice lookup_invoice list_transactions get_balance get_info"
|
||||
)
|
||||
|
||||
uri_str = l1.rpc.call("nip47-create", ["test2", 0])["uri"]
|
||||
LOGGER.info(uri_str)
|
||||
uri = NostrWalletConnectUri.parse(uri_str)
|
||||
nwc = Nwc(uri)
|
||||
get_info = await nwc.get_info()
|
||||
assert get_info.methods == [
|
||||
"make_invoice",
|
||||
"lookup_invoice",
|
||||
"list_transactions",
|
||||
"get_balance",
|
||||
"get_info",
|
||||
]
|
||||
|
||||
signer = NostrSigner.keys(Keys(uri.secret()))
|
||||
client = Client(signer)
|
||||
await client.add_relay(f"ws://{url}")
|
||||
await client.connect()
|
||||
|
||||
response_filter = Filter().kind(Kind(13194)).author(uri.public_key())
|
||||
events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10))
|
||||
start_time = datetime.now()
|
||||
while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10):
|
||||
time.sleep(1)
|
||||
events = await client.fetch_events(
|
||||
response_filter, timeout=timedelta(seconds=1)
|
||||
)
|
||||
assert events.len() == 1
|
||||
assert (
|
||||
events.to_vec()[0].content()
|
||||
== "make_invoice lookup_invoice list_transactions get_balance get_info"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -1066,6 +1118,129 @@ async def test_persistency(node_factory, get_plugin, nostr_client): # noqa: F81
|
|||
assert list["test1"]["budget_msat"] == 3000
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
|
||||
@pytest.mark.asyncio
|
||||
async def test_budget_command(node_factory, get_plugin, nostr_client): # noqa: F811
|
||||
nostr_client, relay_port = nostr_client
|
||||
url = f"127.0.0.1:{relay_port}"
|
||||
l1, l2 = node_factory.line_graph(
|
||||
2,
|
||||
wait_for_announce=True,
|
||||
opts=[
|
||||
{
|
||||
"log-level": "debug",
|
||||
"plugin": get_plugin,
|
||||
"nip47-relays": f"ws://{url}",
|
||||
},
|
||||
{"log-level": "debug"},
|
||||
],
|
||||
)
|
||||
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
||||
LOGGER.info(uri_str)
|
||||
invoice = l2.rpc.call(
|
||||
"invoice",
|
||||
{"label": generate_random_label(), "description": "test1", "amount_msat": 5000},
|
||||
)
|
||||
uri = NostrWalletConnectUri.parse(uri_str)
|
||||
nwc = Nwc(uri)
|
||||
balance = await nwc.get_balance()
|
||||
assert balance == 3000
|
||||
|
||||
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
||||
await nwc.pay_invoice(
|
||||
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
||||
)
|
||||
|
||||
l1.rpc.call("nip47-budget", ["test1", 4000])
|
||||
balance = await nwc.get_balance()
|
||||
assert balance == 4000
|
||||
|
||||
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
||||
await nwc.pay_invoice(
|
||||
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
||||
)
|
||||
|
||||
l1.rpc.call("nip47-budget", ["test1", 5000, "15s"])
|
||||
balance = await nwc.get_balance()
|
||||
assert balance == 5000
|
||||
|
||||
with pytest.raises(
|
||||
RpcError, match="`budget_msat` must be greater than 0 if you use `interval`"
|
||||
):
|
||||
l1.rpc.call("nip47-budget", ["test1", 0, "1s"])
|
||||
|
||||
pay = await nwc.pay_invoice(
|
||||
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
||||
)
|
||||
assert pay.preimage is not None
|
||||
|
||||
balance = await nwc.get_balance()
|
||||
assert balance == 0
|
||||
|
||||
get_info = await nwc.get_info()
|
||||
assert get_info.methods == [
|
||||
"pay_invoice",
|
||||
"multi_pay_invoice",
|
||||
"pay_keysend",
|
||||
"multi_pay_keysend",
|
||||
"make_invoice",
|
||||
"lookup_invoice",
|
||||
"list_transactions",
|
||||
"get_balance",
|
||||
"get_info",
|
||||
]
|
||||
|
||||
signer = NostrSigner.keys(Keys(uri.secret()))
|
||||
client = Client(signer)
|
||||
await client.add_relay(f"ws://{url}")
|
||||
await client.connect()
|
||||
|
||||
response_filter = Filter().kind(Kind(13194)).author(uri.public_key())
|
||||
events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10))
|
||||
start_time = datetime.now()
|
||||
while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10):
|
||||
time.sleep(1)
|
||||
events = await client.fetch_events(
|
||||
response_filter, timeout=timedelta(seconds=1)
|
||||
)
|
||||
assert events.len() == 1
|
||||
assert (
|
||||
events.to_vec()[0].content()
|
||||
== "pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_invoice lookup_invoice list_transactions get_balance get_info"
|
||||
)
|
||||
|
||||
time.sleep(16)
|
||||
|
||||
balance = await nwc.get_balance()
|
||||
assert balance == 5000
|
||||
|
||||
l1.rpc.call("nip47-budget", ["test1", 0])
|
||||
balance = await nwc.get_balance()
|
||||
assert balance == 0
|
||||
|
||||
get_info = await nwc.get_info()
|
||||
assert get_info.methods == [
|
||||
"make_invoice",
|
||||
"lookup_invoice",
|
||||
"list_transactions",
|
||||
"get_balance",
|
||||
"get_info",
|
||||
]
|
||||
|
||||
events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10))
|
||||
start_time = datetime.now()
|
||||
while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10):
|
||||
time.sleep(1)
|
||||
events = await client.fetch_events(
|
||||
response_filter, timeout=timedelta(seconds=1)
|
||||
)
|
||||
assert events.len() == 1
|
||||
assert (
|
||||
events.to_vec()[0].content()
|
||||
== "make_invoice lookup_invoice list_transactions get_balance get_info"
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def nostr_client(nostr_relay):
|
||||
port = nostr_relay
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue