refactor, clippy and ruff fixes

This commit is contained in:
daywalker90 2026-07-26 14:48:42 +02:00
parent f6517cab7e
commit 61c07f6079
10 changed files with 1134 additions and 934 deletions

View file

@ -165,8 +165,8 @@ async fn shutdown_handler(
_args: serde_json::Value,
) -> Result<(), anyhow::Error> {
let mut locked_handles = plugin.state().handles.lock().await;
for (_x, (client, _client_pubkey, _wallet_keys)) in locked_handles.drain() {
client.shutdown().await;
for (_x, wallet_service) in locked_handles.drain() {
wallet_service.client.shutdown().await;
}
std::process::exit(0)
}
@ -299,6 +299,6 @@ fn do_certificates_exist(cert_dir: &Path) -> bool {
required_files.iter().all(|file| {
let path = cert_dir.join(file);
path.exists() && path.metadata().map(|m| m.len() > 0).unwrap_or(false)
path.exists() && path.metadata().is_ok_and(|m| m.len() > 0)
})
}

View file

@ -38,11 +38,12 @@ use crate::{
nwc_keysend::pay_keysend_response,
nwc_lookups::{list_transactions_response, lookup_invoice_response},
nwc_pay::pay_invoice_response,
structs::{NwcStore, PluginState},
structs::{NwcStore, PluginState, WalletService},
tasks::budget_task,
util::{build_capabilities, build_notifications_vec, is_read_only_nwc},
};
#[allow(clippy::too_many_lines)]
pub async fn run_nwc(
plugin: Plugin<PluginState>,
label: String,
@ -154,10 +155,12 @@ pub async fn run_nwc(
});
let mut locked_handles = plugin.state().handles.lock().await;
locked_handles.insert(
label.clone(),
(nostr_client, client_keys.public_key(), wallet_keys),
);
let wallet_service = WalletService {
client: nostr_client,
client_pubkey: client_keys.public_key(),
wallet_secret: wallet_keys,
};
locked_handles.insert(label.clone(), wallet_service);
Ok(())
}
@ -208,8 +211,8 @@ pub async fn send_nwc_info_event(
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, _wallet_secret)) = locked_handles.remove(label) {
client.shutdown().await;
if let Some(wallet_service) = locked_handles.remove(label) {
wallet_service.client.shutdown().await;
}
stop_nwc_budget_job(&plugin, label);

View file

@ -2,13 +2,14 @@ use std::{collections::HashMap, str::FromStr};
use cln_plugin::Plugin;
use cln_rpc::{
ClnRpc,
model::requests::{KeysendRequest, XkeysendRequest},
primitives::{Amount, PublicKey, TlvEntry, TlvStream},
};
use nostr::nips::nip47;
use nostr::nips::nip47::{self};
use crate::{
structs::PluginState,
structs::{NwcStore, PluginState},
util::{at_or_above_version, budget_amount_check, load_nwc_store, update_nwc_store},
};
@ -57,7 +58,7 @@ async fn pay_keysend(
});
}
let mut nwc_store = load_nwc_store(&mut rpc, label)
let nwc_store = load_nwc_store(&mut rpc, label)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
@ -82,141 +83,161 @@ async fn pay_keysend(
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(),
}),
},
None => Err(nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
}),
},
}
xkeysend(&mut rpc, params, pubkey, nwc_store, label).await
} 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(),
}),
},
}
keysend(&mut rpc, params, pubkey, nwc_store, label).await
}
}
async fn xkeysend(
rpc: &mut ClnRpc,
params: nip47::PayKeysendRequest,
pubkey: PublicKey,
mut nwc_store: NwcStore,
label: &str,
) -> Result<nip47::PayKeysendResponse, nip47::NIP47Error> {
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(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(),
}),
},
None => Err(nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
}),
},
}
}
async fn keysend(
rpc: &mut ClnRpc,
params: nip47::PayKeysendRequest,
pubkey: PublicKey,
mut nwc_store: NwcStore,
label: &str,
) -> Result<nip47::PayKeysendResponse, nip47::NIP47Error> {
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(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(),
}),
},
}
}

View file

@ -2,9 +2,11 @@ use std::{cmp::Reverse, str::FromStr};
use cln_plugin::Plugin;
use cln_rpc::{
ClnRpc,
model::{
requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest},
responses::{
DecodeResponse,
DecodeType,
ListinvoicesInvoices,
ListinvoicesInvoicesStatus,
@ -13,13 +15,13 @@ use cln_rpc::{
},
},
primitives::Sha256,
ClnRpc,
};
use nostr::{nips::nip47, Timestamp};
use nostr::{Timestamp, nips::nip47};
use tonic::transport::Channel;
use crate::{
hold::{self, list_request::Constraint, ListRequest},
structs::{PluginState, NOT_INV_ERR},
hold::{self, ListRequest, hold_client::HoldClient, list_request::Constraint},
structs::{NOT_INV_ERR, PluginState},
};
pub async fn lookup_invoice_response(
@ -127,186 +129,7 @@ async fn lookup_invoice(
if holdinvoice_support {
let mut hold_client = plugin.state().hold_client.lock().clone().unwrap();
let (hold_invoice, invoice_decoded) = if let Some(ph) = &params.payment_hash {
let payment_hash_hash = match hex::decode(ph) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash".to_owned(),
})
}
};
let list_request = ListRequest {
constraint: Some(Constraint::PaymentHash(payment_hash_hash)),
};
let hold_lookup = hold_client
.list(list_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoice: {e}"),
})?
.into_inner();
if hold_lookup.invoices.len() != 1 {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Transaction not found".to_owned(),
});
}
let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap();
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
(hold_invoice, invoice_decoded)
} else {
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: params.invoice.unwrap(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
let ph = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_payment_hash.unwrap(),
DecodeType::BOLT11_INVOICE => invoice_decoded.payment_hash.unwrap().to_string(),
_ => todo!(),
};
let payment_hash_hash = match hex::decode(&ph) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash in invoice".to_owned(),
})
}
};
let list_request = ListRequest {
constraint: Some(Constraint::PaymentHash(payment_hash_hash)),
};
let hold_lookup = hold_client
.list(list_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoice: {e}"),
})?
.into_inner();
if hold_lookup.invoices.len() != 1 {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Transaction not found".to_owned(),
});
}
let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap();
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
(hold_invoice, invoice_decoded)
};
let not_invoice_err = Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: NOT_INV_ERR.to_owned(),
});
let description = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description,
DecodeType::BOLT11_INVOICE => invoice_decoded.description,
_ => return not_invoice_err,
};
let description_hash = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => None,
DecodeType::BOLT11_INVOICE => invoice_decoded.description_hash.map(|h| h.to_string()),
_ => return not_invoice_err,
};
let created_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap())
}
DecodeType::BOLT11_INVOICE => Timestamp::from_secs(invoice_decoded.created_at.unwrap()),
_ => return not_invoice_err,
};
let amount = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_amount_msat.unwrap().msat(),
DecodeType::BOLT11_INVOICE => {
if let Some(amt) = invoice_decoded.amount_msat {
amt.msat()
} else {
// amount: `any` but have to put a value...
0
}
}
_ => return not_invoice_err,
};
let expires_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded
.invoice_relative_expiry
.map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))),
DecodeType::BOLT11_INVOICE => invoice_decoded
.expiry
.map(|e_at| created_at + Timestamp::from_secs(e_at)),
_ => return not_invoice_err,
};
let state = match hold_invoice.state() {
hold::InvoiceState::Unpaid => nip47::TransactionState::Pending,
hold::InvoiceState::Accepted => nip47::TransactionState::Accepted,
hold::InvoiceState::Paid => nip47::TransactionState::Settled,
hold::InvoiceState::Cancelled => nip47::TransactionState::Expired,
};
let settled_at = if hold_invoice.settled_at() != 0 {
Some(Timestamp::from_secs(hold_invoice.settled_at()))
} else {
None
};
let preimage = if hold_invoice.state() == hold::InvoiceState::Paid {
Some(hex::encode(hold_invoice.preimage()))
} else {
None
};
return Ok(nip47::LookupInvoiceResponse {
transaction_type: Some(nip47::TransactionType::Incoming),
invoice: Some(hold_invoice.invoice.clone()),
description,
description_hash,
preimage,
payment_hash: hex::encode(hold_invoice.payment_hash),
amount,
fees_paid: 0,
created_at,
expires_at,
settled_at,
metadata: None,
state: Some(state),
});
return lookup_holdinvoice(&mut hold_client, &mut rpc, params).await;
}
Err(nip47::NIP47Error {
@ -315,6 +138,202 @@ async fn lookup_invoice(
})
}
async fn lookup_holdinvoice(
hold_client: &mut HoldClient<Channel>,
rpc: &mut ClnRpc,
params: nip47::LookupInvoiceRequest,
) -> Result<nip47::LookupInvoiceResponse, nip47::NIP47Error> {
log::debug!("Looking up hold invoice for params {params:#?}");
let (hold_invoice, invoice_decoded) =
get_and_decode_holdinvoice(rpc, hold_client, params).await?;
let not_invoice_err = Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: NOT_INV_ERR.to_owned(),
});
let description = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description,
DecodeType::BOLT11_INVOICE => invoice_decoded.description,
_ => return not_invoice_err,
};
let description_hash = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => None,
DecodeType::BOLT11_INVOICE => invoice_decoded.description_hash.map(|h| h.to_string()),
_ => return not_invoice_err,
};
let created_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap())
}
DecodeType::BOLT11_INVOICE => Timestamp::from_secs(invoice_decoded.created_at.unwrap()),
_ => return not_invoice_err,
};
let amount = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_amount_msat.unwrap().msat(),
DecodeType::BOLT11_INVOICE => {
if let Some(amt) = invoice_decoded.amount_msat {
amt.msat()
} else {
// amount: `any` but have to put a value...
0
}
}
_ => return not_invoice_err,
};
let expires_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded
.invoice_relative_expiry
.map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))),
DecodeType::BOLT11_INVOICE => invoice_decoded
.expiry
.map(|e_at| created_at + Timestamp::from_secs(e_at)),
_ => return not_invoice_err,
};
let state = match hold_invoice.state() {
hold::InvoiceState::Unpaid => nip47::TransactionState::Pending,
hold::InvoiceState::Accepted => nip47::TransactionState::Accepted,
hold::InvoiceState::Paid => nip47::TransactionState::Settled,
hold::InvoiceState::Cancelled => nip47::TransactionState::Expired,
};
let settled_at = if hold_invoice.settled_at() != 0 {
Some(Timestamp::from_secs(hold_invoice.settled_at()))
} else {
None
};
let preimage = if hold_invoice.state() == hold::InvoiceState::Paid {
Some(hex::encode(hold_invoice.preimage()))
} else {
None
};
Ok(nip47::LookupInvoiceResponse {
transaction_type: Some(nip47::TransactionType::Incoming),
invoice: Some(hold_invoice.invoice.clone()),
description,
description_hash,
preimage,
payment_hash: hex::encode(hold_invoice.payment_hash),
amount,
fees_paid: 0,
created_at,
expires_at,
settled_at,
metadata: None,
state: Some(state),
})
}
async fn get_and_decode_holdinvoice(
rpc: &mut ClnRpc,
hold_client: &mut HoldClient<Channel>,
params: nip47::LookupInvoiceRequest,
) -> Result<(hold::Invoice, DecodeResponse), nip47::NIP47Error> {
if let Some(ph) = &params.payment_hash {
let payment_hash_hash = match hex::decode(ph) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash".to_owned(),
});
}
};
let list_request = ListRequest {
constraint: Some(Constraint::PaymentHash(payment_hash_hash)),
};
let hold_lookup = hold_client
.list(list_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoice: {e}"),
})?
.into_inner();
if hold_lookup.invoices.len() != 1 {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Transaction not found".to_owned(),
});
}
let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap();
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
Ok((hold_invoice, invoice_decoded))
} else {
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: params.invoice.unwrap(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
let ph = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_payment_hash.unwrap(),
DecodeType::BOLT11_INVOICE => invoice_decoded.payment_hash.unwrap().to_string(),
_ => todo!(),
};
let payment_hash_hash = match hex::decode(&ph) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash in invoice".to_owned(),
});
}
};
let list_request = ListRequest {
constraint: Some(Constraint::PaymentHash(payment_hash_hash)),
};
let hold_lookup = hold_client
.list(list_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoice: {e}"),
})?
.into_inner();
if hold_lookup.invoices.len() != 1 {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Transaction not found".to_owned(),
});
}
let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap();
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
Ok((hold_invoice, invoice_decoded))
}
}
pub async fn list_transactions_response(
plugin: Plugin<PluginState>,
params: nip47::ListTransactionsRequest,
@ -389,112 +408,8 @@ async fn list_transactions(
if holdinvoice_support {
let mut hold_client = plugin.state().hold_client.lock().clone().unwrap();
let lookup_request = ListRequest { constraint: None };
let hold_lookup = hold_client
.list(lookup_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoices: {e}"),
})?
.into_inner();
for hold_invoice in hold_lookup.invoices {
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
let description = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description,
DecodeType::BOLT11_INVOICE => invoice_decoded.description,
_ => continue,
};
let description_hash = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => None,
DecodeType::BOLT11_INVOICE => {
invoice_decoded.description_hash.map(|h| h.to_string())
}
_ => continue,
};
let created_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap())
}
DecodeType::BOLT11_INVOICE => {
Timestamp::from_secs(invoice_decoded.created_at.unwrap())
}
_ => continue,
};
let amount = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
invoice_decoded.invoice_amount_msat.unwrap().msat()
}
DecodeType::BOLT11_INVOICE => {
if let Some(amt) = invoice_decoded.amount_msat {
amt.msat()
} else {
// amount: `any` but have to put a value...
0
}
}
_ => continue,
};
let expires_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded
.invoice_relative_expiry
.map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))),
DecodeType::BOLT11_INVOICE => invoice_decoded
.expiry
.map(|e_at| created_at + Timestamp::from_secs(e_at)),
_ => continue,
};
let state = match hold_invoice.state() {
hold::InvoiceState::Unpaid => nip47::TransactionState::Pending,
hold::InvoiceState::Accepted => nip47::TransactionState::Accepted,
hold::InvoiceState::Paid => nip47::TransactionState::Settled,
hold::InvoiceState::Cancelled => nip47::TransactionState::Expired,
};
let settled_at = if hold_invoice.settled_at() != 0 {
Some(Timestamp::from_secs(hold_invoice.settled_at()))
} else {
None
};
let preimage = if hold_invoice.state() == hold::InvoiceState::Paid {
Some(hex::encode(hold_invoice.preimage()))
} else {
None
};
transactions.push(nip47::LookupInvoiceResponse {
transaction_type: Some(nip47::TransactionType::Incoming),
invoice: Some(hold_invoice.invoice.clone()),
description,
description_hash,
preimage,
payment_hash: hex::encode(hold_invoice.payment_hash),
amount,
fees_paid: 0,
created_at,
expires_at,
settled_at,
metadata: None,
state: Some(state),
});
}
list_holdinvoices_to_transactions(&mut hold_client, &mut rpc, &mut transactions)
.await?;
}
}
@ -548,6 +463,114 @@ async fn list_transactions(
Ok(transactions)
}
async fn list_holdinvoices_to_transactions(
hold_client: &mut HoldClient<Channel>,
rpc: &mut ClnRpc,
transactions: &mut Vec<nip47::LookupInvoiceResponse>,
) -> Result<(), nip47::NIP47Error> {
let lookup_request = ListRequest { constraint: None };
let hold_lookup = hold_client
.list(lookup_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoices: {e}"),
})?
.into_inner();
for hold_invoice in hold_lookup.invoices {
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
let description = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description,
DecodeType::BOLT11_INVOICE => invoice_decoded.description,
_ => continue,
};
let description_hash = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => None,
DecodeType::BOLT11_INVOICE => invoice_decoded.description_hash.map(|h| h.to_string()),
_ => continue,
};
let created_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap())
}
DecodeType::BOLT11_INVOICE => Timestamp::from_secs(invoice_decoded.created_at.unwrap()),
_ => continue,
};
let amount = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_amount_msat.unwrap().msat(),
DecodeType::BOLT11_INVOICE => {
if let Some(amt) = invoice_decoded.amount_msat {
amt.msat()
} else {
// amount: `any` but have to put a value...
0
}
}
_ => continue,
};
let expires_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded
.invoice_relative_expiry
.map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))),
DecodeType::BOLT11_INVOICE => invoice_decoded
.expiry
.map(|e_at| created_at + Timestamp::from_secs(e_at)),
_ => continue,
};
let state = match hold_invoice.state() {
hold::InvoiceState::Unpaid => nip47::TransactionState::Pending,
hold::InvoiceState::Accepted => nip47::TransactionState::Accepted,
hold::InvoiceState::Paid => nip47::TransactionState::Settled,
hold::InvoiceState::Cancelled => nip47::TransactionState::Expired,
};
let settled_at = if hold_invoice.settled_at() != 0 {
Some(Timestamp::from_secs(hold_invoice.settled_at()))
} else {
None
};
let preimage = if hold_invoice.state() == hold::InvoiceState::Paid {
Some(hex::encode(hold_invoice.preimage()))
} else {
None
};
transactions.push(nip47::LookupInvoiceResponse {
transaction_type: Some(nip47::TransactionType::Incoming),
invoice: Some(hold_invoice.invoice.clone()),
description,
description_hash,
preimage,
payment_hash: hex::encode(hold_invoice.payment_hash),
amount,
fees_paid: 0,
created_at,
expires_at,
settled_at,
metadata: None,
state: Some(state),
});
}
Ok(())
}
async fn make_lookup_response_from_listinvoices(
rpc: &mut ClnRpc,
list_invoice: ListinvoicesInvoices,
@ -557,10 +580,12 @@ async fn make_lookup_response_from_listinvoices(
message: NOT_INV_ERR.to_owned(),
});
let invstring = if list_invoice.bolt11.is_some() {
list_invoice.bolt11.as_ref().unwrap()
let invstring = if let Some(bolt11) = list_invoice.bolt11 {
bolt11
} else if let Some(bolt12) = list_invoice.bolt12 {
bolt12
} else {
list_invoice.bolt12.as_ref().unwrap()
return not_invoice_err;
};
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
@ -628,7 +653,7 @@ async fn make_lookup_response_from_listinvoices(
Ok(nip47::LookupInvoiceResponse {
transaction_type: Some(nip47::TransactionType::Incoming),
invoice: Some(invstring.to_owned()),
invoice: Some(invstring),
description,
description_hash,
preimage,

View file

@ -22,15 +22,13 @@ use nostr::{
Tag,
Timestamp,
event::FinalizeEventAsync,
key::{Keys, PublicKey, SecretKey},
nips::{nip04, nip44, nip47},
};
use nostr_sdk::client::Client;
use crate::{
OPT_NOTIFICATIONS,
hold::{InvoiceState, ListRequest, TrackRequest, list_request::Constraint},
structs::{NOT_INV_ERR, PluginState},
structs::{NOT_INV_ERR, PluginState, WalletService},
};
pub async fn payment_received_handler(
@ -66,10 +64,14 @@ pub async fn payment_received_handler(
let invoice = invoice_resp
.first()
.ok_or_else(|| anyhow!("invoice not found"))?;
let invstring = if invoice.bolt11.is_some() {
invoice.bolt11.as_ref().unwrap()
let invstring = if let Some(bolt11) = &invoice.bolt11 {
bolt11.clone()
} else if let Some(bolt12) = &invoice.bolt12 {
bolt12.clone()
} else {
invoice.bolt12.as_ref().unwrap()
return Err(anyhow!(
"Listinvoices has neither returned bolt11 or bolt12 field"
));
};
let invoice_decoded = rpc
@ -83,8 +85,8 @@ pub async fn payment_received_handler(
let clients = plugin.state().handles.lock().await;
for (client, client_pubkey, wallet_secret) in clients.values() {
send_notification(&notification, client, client_pubkey.clone(), wallet_secret).await?;
for wallet_service in clients.values() {
send_notification(&notification, wallet_service).await?;
}
Ok(())
@ -92,7 +94,7 @@ pub async fn payment_received_handler(
fn make_payment_received_from_listinvoices(
invoice: &ListinvoicesInvoices,
invstring: &str,
invstring: String,
invoice_decoded: DecodeResponse,
) -> Result<String, anyhow::Error> {
let not_invoice_err = Err(anyhow!(NOT_INV_ERR.to_owned()));
@ -160,7 +162,7 @@ fn make_payment_received_from_listinvoices(
notification_type: nip47::NotificationType::PaymentReceived,
notification: nip47::NotificationResult::PaymentReceived(nip47::PaymentNotification {
transaction_type: Some(nip47::TransactionType::Incoming),
invoice: invstring.to_owned(),
invoice: invstring,
description: description.clone(),
description_hash: description_hash.clone(),
preimage: preimage.clone(),
@ -220,8 +222,8 @@ pub async fn payment_sent_handler(
let clients = plugin.state().handles.lock().await;
for (client, client_pubkey, wallet_secret) in clients.values() {
send_notification(&notification, client, client_pubkey.clone(), wallet_secret).await?;
for wallet_service in clients.values() {
send_notification(&notification, wallet_service).await?;
}
Ok(())
@ -342,18 +344,19 @@ async fn make_payment_sent_from_listpays(
async fn send_notification(
notification: &String,
client: &Client,
client_pubkey: PublicKey,
wallet_secret: &Keys,
wallet_service: &WalletService,
) -> Result<(), anyhow::Error> {
log::debug!("NOTIFICATION: {notification}");
let content_encrypted_nip04 =
nip04::encrypt(wallet_secret.secret_key(), &client_pubkey, notification)?;
let content_encrypted_nip04 = nip04::encrypt(
wallet_service.wallet_secret.secret_key(),
&wallet_service.client_pubkey,
notification,
)?;
let event_nip04 = EventBuilder::new(Kind::from_u16(23196), content_encrypted_nip04)
.tag(Tag::public_key(client_pubkey))
.finalize_async(wallet_secret)
.tag(Tag::public_key(wallet_service.client_pubkey))
.finalize_async(&wallet_service.wallet_secret)
.await?;
let nip04_result = client.send_event(&event_nip04).await?;
let nip04_result = wallet_service.client.send_event(&event_nip04).await?;
if nip04_result.success.is_empty() {
log::warn!(
"None of the relays accepted our nip04 notification: {}",
@ -367,16 +370,16 @@ async fn send_notification(
log::debug!("NIP04 NOTIFICATION SENT: {event_nip04:?}");
let content_encrypted_nip44 = nip44::encrypt(
wallet_secret.secret_key(),
&client_pubkey,
wallet_service.wallet_secret.secret_key(),
&wallet_service.client_pubkey,
notification,
nip44::Version::V2,
)?;
let event_nip44 = EventBuilder::new(Kind::from_u16(23197), content_encrypted_nip44)
.tag(Tag::public_key(client_pubkey))
.finalize_async(wallet_secret)
.tag(Tag::public_key(wallet_service.client_pubkey))
.finalize_async(&wallet_service.wallet_secret)
.await?;
let nip44_result = client.send_event(&event_nip44).await?;
let nip44_result = wallet_service.client.send_event(&event_nip44).await?;
if nip44_result.success.is_empty() {
log::warn!(
"None of the relays accepted our nip44 notification: {}",
@ -392,6 +395,7 @@ async fn send_notification(
Ok(())
}
#[allow(clippy::too_many_lines)]
pub async fn holdinvoice_accepted_handler(
plugin: Plugin<PluginState>,
payment_hash: Vec<u8>,
@ -514,8 +518,8 @@ pub async fn holdinvoice_accepted_handler(
};
let notification = serde_json::to_string(&content).unwrap();
for (client, client_pubkey, wallet_secret) in clients.values() {
send_notification(&notification, client, client_pubkey.clone(), wallet_secret).await?;
for wallet_service in clients.values() {
send_notification(&notification, wallet_service).await?;
}
Ok(())
}

View file

@ -155,7 +155,7 @@ pub async fn nwc_budget(
clients
.get(&label)
.ok_or_else(|| anyhow!("No client found for label: {label}"))?
.0
.client
.clone(),
method_capabilities,
wallet_keys,

View file

@ -15,8 +15,7 @@ pub const NOT_INV_ERR: &str = "Not an invoice or invalid invoice";
#[derive(Clone)]
pub struct PluginState {
pub config: Arc<Mutex<Config>>,
pub handles:
Arc<tokio::sync::Mutex<HashMap<String, (client::Client, nostr::PublicKey, nostr::Keys)>>>,
pub handles: Arc<tokio::sync::Mutex<HashMap<String, WalletService>>>,
pub rpc_lock: Arc<tokio::sync::Mutex<ClnRpc>>,
pub budget_jobs: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>,
pub hold_client: Arc<Mutex<Option<HoldClient<Channel>>>>,
@ -33,6 +32,12 @@ impl PluginState {
}
}
pub struct WalletService {
pub client: client::Client,
pub client_pubkey: nostr::PublicKey,
pub wallet_secret: nostr::Keys,
}
#[derive(Clone, Debug)]
pub struct Config {
pub relays: Vec<RelayUrl>,

View file

@ -6,12 +6,12 @@ description = "python dependencies for running tests"
[dependency-groups]
dev = [
"pytest>=7.4,<9",
"pytest>=8,<10",
"pytest-asyncio>=0.23.8,<2",
"pytest-xdist>=3.7,<4",
"pytest-timeout>=2.4,<3",
"nostr-sdk>=0.44",
"pyln-testing>=25.5",
"pyln-client>=25.5",
"pyln-proto>=25.5",
"pyln-testing>=25.9",
"pyln-client>=25.9",
"pyln-proto>=25.9",
]

109
tests/test_cln-nip47.py Executable file → Normal file
View file

@ -1,43 +1,46 @@
# ruff: noqa: DTZ005
import asyncio
import hashlib
import inspect
import json
import logging
import time
import asyncio
import secrets
import time
import uuid
from collections.abc import Awaitable, Callable
from datetime import datetime, timedelta
from typing import Any, Awaitable, Callable, Union
from typing import Any, Union
import pytest
from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import RpcError, wait_for, TIMEOUT
from util import generate_random_label, get_plugin, get_hold # noqa: F401
from nostr_sdk import (
Client,
RelayUrl,
Event,
EventBuilder,
Filter,
Event,
HandleNotification,
Keys,
KeysendTlvRecord,
Kind,
HandleNotification,
ListTransactionsRequest,
LookupInvoiceRequest,
MakeInvoiceRequest,
Method,
NostrSdkError,
NostrSigner,
NostrWalletConnectUri,
Nwc,
PayInvoiceRequest,
PayKeysendRequest,
PublicKey,
RelayUrl,
Tag,
TagKind,
Method,
PublicKey,
TransactionType,
)
from pyln.testing.fixtures import *
from pyln.testing.utils import TIMEOUT, RpcError, wait_for
from util import generate_random_label, get_hold, get_plugin # noqa: F401
LOGGER = logging.getLogger(__name__)
@ -83,7 +86,7 @@ async def fetch_event_responses(
handler = NotificationHandler(events, stop_after)
task = asyncio.create_task(client.handle_notifications(handler))
time.sleep(1)
await asyncio.sleep(1)
if inspect.iscoroutine(action):
action_result = await action
elif inspect.iscoroutinefunction(action):
@ -123,7 +126,7 @@ async def fetch_info_event(
while events.len() < 1 and (datetime.now() - start_time) < timedelta(
seconds=TIMEOUT
):
time.sleep(1)
await asyncio.sleep(1)
events = await client.fetch_events(
response_filter, timeout=timedelta(seconds=1)
)
@ -135,7 +138,7 @@ async def fetch_info_event(
@pytest.mark.asyncio
async def test_get_balance(nostr_relay, node_factory, get_plugin): # noqa: F811
url = nostr_relay
l1, l2 = node_factory.line_graph(
l1, _l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
@ -237,7 +240,7 @@ async def test_get_info(nostr_relay, node_factory, get_plugin): # noqa: F811
},
)
l1.daemon.wait_for_log("All NWC's loaded")
time.sleep(5)
await asyncio.sleep(5)
await client.connect()
info_event = await fetch_info_event(client, uri)
get_info = await nwc.get_info()
@ -341,7 +344,7 @@ async def test_make_invoice(nostr_relay, node_factory, get_plugin): # noqa: F81
MakeInvoiceRequest(
amount=3001,
description="test2",
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
description_hash=hashlib.sha256(b"test2").hexdigest(),
expiry=120,
)
)
@ -367,7 +370,7 @@ async def test_make_invoice(nostr_relay, node_factory, get_plugin): # noqa: F81
MakeInvoiceRequest(
amount=3001,
description=None,
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
description_hash=hashlib.sha256(b"test2").hexdigest(),
expiry=120,
)
)
@ -378,7 +381,7 @@ async def test_make_invoice(nostr_relay, node_factory, get_plugin): # noqa: F81
MakeInvoiceRequest(
amount=3001,
description="test1",
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
description_hash=hashlib.sha256(b"test2").hexdigest(),
expiry=120,
)
)
@ -562,7 +565,7 @@ async def test_lookup_invoice(nostr_relay, node_factory, get_plugin): # noqa: F
MakeInvoiceRequest(
amount=3001,
description="test2",
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
description_hash=hashlib.sha256(b"test2").hexdigest(),
expiry=1000,
)
)
@ -584,9 +587,7 @@ async def test_lookup_invoice(nostr_relay, node_factory, get_plugin): # noqa: F
assert invoice_lookup.created_at.as_secs() == pytest.approx(
invoice_decode["created_at"], abs=3
)
assert (
invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest()
)
assert invoice_lookup.description_hash == hashlib.sha256(b"test2").hexdigest()
assert invoice_lookup.expires_at.as_secs() == pytest.approx(
listpays_rpc["expires_at"], abs=3
)
@ -613,9 +614,7 @@ async def test_lookup_invoice(nostr_relay, node_factory, get_plugin): # noqa: F
assert invoice_lookup.created_at.as_secs() == pytest.approx(
invoice_decode["created_at"], abs=3
)
assert (
invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest()
)
assert invoice_lookup.description_hash == hashlib.sha256(b"test2").hexdigest()
assert invoice_lookup.expires_at.as_secs() == pytest.approx(
listpays_rpc["expires_at"], abs=3
)
@ -693,9 +692,9 @@ async def test_list_transactions(nostr_relay, node_factory, get_plugin): # noqa
],
)
l1.rpc.call(
"pay",
"xpay",
{
"bolt11": l2.rpc.call(
"invstring": l2.rpc.call(
"invoice",
{
"amount_msat": 500000000,
@ -710,7 +709,7 @@ async def test_list_transactions(nostr_relay, node_factory, get_plugin): # noqa
l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][
"spendable_msat"
]
> 30001
> 400000000
)
)
uri_str = l1.rpc.call("nip47-create", ["test1"])["uri"]
@ -741,7 +740,7 @@ async def test_list_transactions(nostr_relay, node_factory, get_plugin): # noqa
amount=3000, description="test2", description_hash=None, expiry=None
)
)
result = l2.rpc.call("pay", [invoice.invoice])
result = l2.rpc.call("xpay", [invoice.invoice])
invoice = await nwc.make_invoice(
MakeInvoiceRequest(
@ -762,19 +761,23 @@ async def test_list_transactions(nostr_relay, node_factory, get_plugin): # noqa
)
assert len(result) == 22
for tx in result:
tx.description is not None
tx.invoice is not None
tx.amount is not None
tx.created_at is not None
tx.description_hash is None
tx.expires_at is None
tx.preimage is not None
tx.settled_at is not None
tx.metadata is None
tx.transaction_type is not None
tx.state is not None
tx.payment_hash is not None
tx.fees_paid is not None
assert tx.description is not None
assert tx.invoice is not None
assert tx.amount is not None
assert tx.created_at is not None
assert tx.description_hash is None
assert tx.preimage is not None
assert tx.settled_at is not None
assert tx.metadata is None
assert tx.transaction_type is not None
assert tx.state is not None
assert tx.payment_hash is not None
assert tx.fees_paid is not None
if tx.transaction_type == TransactionType.INCOMING:
assert tx.expires_at is not None
else:
assert tx.expires_at is None
@pytest.mark.asyncio
@ -927,7 +930,7 @@ async def test_notifications(nostr_relay, node_factory, get_plugin): # noqa: F8
},
)
l1.daemon.wait_for_log("All NWC's loaded")
time.sleep(3)
await asyncio.sleep(3)
await client.connect()
await fetch_info_event(client, uri)
@ -940,7 +943,7 @@ async def test_notifications(nostr_relay, node_factory, get_plugin): # noqa: F8
},
)
with pytest.raises(AssertionError, match="0 == 1"):
(responses3, pay3) = await fetch_event_responses(
(_responses3, _pay3) = await fetch_event_responses(
client,
client_pubkey,
23196,
@ -1036,7 +1039,7 @@ async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811
},
)
l1.daemon.wait_for_log("All NWC's loaded")
time.sleep(3)
await asyncio.sleep(3)
uri = NostrWalletConnectUri.parse(uri_str)
signer = NostrSigner.keys(Keys(uri.secret()))
client = Client(signer)
@ -1066,7 +1069,7 @@ async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811
},
)
l1.daemon.wait_for_log("All NWC's loaded")
time.sleep(3)
await asyncio.sleep(3)
await client.connect()
await fetch_info_event(client, uri)
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
@ -1107,7 +1110,7 @@ async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811
PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"])
)
time.sleep(11)
await asyncio.sleep(11)
list = l1.rpc.call("nip47-list", ["test1"])[0]
assert list["test1"]["budget_msat"] == 3000
@ -1138,7 +1141,7 @@ async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811
},
)
l1.daemon.wait_for_log("All NWC's loaded")
time.sleep(3)
await asyncio.sleep(3)
await client.connect()
await fetch_info_event(client, uri)
@ -1147,7 +1150,7 @@ async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811
PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"])
)
time.sleep(11)
await asyncio.sleep(11)
list = l1.rpc.call("nip47-list", ["test1"])[0]
assert list["test1"]["budget_msat"] == 3000
@ -1242,7 +1245,7 @@ async def test_budget_command(nostr_relay, node_factory, get_plugin): # noqa: F
== "payment_received payment_sent"
)
time.sleep(18)
await asyncio.sleep(18)
balance = await nwc.get_balance()
assert balance == 5000
@ -1706,7 +1709,7 @@ async def test_hold_invoice(
start_time = datetime.now()
while (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
await asyncio.sleep(1)
try:
await nwc.lookup_invoice(
LookupInvoiceRequest(
@ -1757,7 +1760,7 @@ async def test_hold_invoice(
start_time = datetime.now()
while (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
await asyncio.sleep(1)
try:
await nwc.lookup_invoice(
LookupInvoiceRequest(

949
tests/uv.lock generated

File diff suppressed because it is too large Load diff