notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 528237343466cad46d536d90ccef60ee1a6c6d4e
parent 14c59a6c944c448b0c38a537cb1a20124df7d189
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon,  1 Sep 2025 14:13:22 -0400

use `PayCache` when zapping

to avoid needlessly querying ln endpoint

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/zaps/cache.rs | 65++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/notedeck/src/zaps/networking.rs | 222++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
2 files changed, 171 insertions(+), 116 deletions(-)

diff --git a/crates/notedeck/src/zaps/cache.rs b/crates/notedeck/src/zaps/cache.rs @@ -11,16 +11,12 @@ use crate::{ get_wallet_for, zaps::{ get_users_zap_address, - networking::{LNUrlPayResponse, PayEntry}, - ZapAddress, + networking::{fetch_invoice_promise, FetchedInvoiceResponse, LNUrlPayResponse, PayEntry}, }, Accounts, GlobalWallet, ZapError, }; -use super::{ - networking::{fetch_invoice_lnurl, fetch_invoice_lud16, FetchedInvoice, FetchingInvoice}, - zap::Zap, -}; +use super::{networking::FetchingInvoice, zap::Zap}; type ZapId = u32; @@ -34,6 +30,8 @@ pub struct Zaps { zaps: std::collections::HashMap<ZapId, ZapState>, in_flight: Vec<ZapPromise>, events: Vec<EventResponse>, + + pay_cache: PayCache, } /// Cache to hold LNURL payRequest responses from the desired LNURL endpoint @@ -56,6 +54,7 @@ impl PayCache { fn process_event( id: ZapId, event: ZapEvent, + cache: &PayCache, accounts: &mut Accounts, global_wallet: &mut GlobalWallet, ndb: &Ndb, @@ -65,7 +64,7 @@ fn process_event( ZapEvent::FetchInvoice { zap_ctx, sender_relays, - } => process_new_zap_event(zap_ctx, accounts, ndb, txn, sender_relays), + } => process_new_zap_event(cache, zap_ctx, accounts, ndb, txn, sender_relays), ZapEvent::SendNWC { zap_ctx, req_noteid, @@ -102,6 +101,7 @@ fn process_event( } fn process_new_zap_event( + cache: &PayCache, zap_ctx: ZapCtx, accounts: &Accounts, ndb: &Ndb, @@ -125,6 +125,7 @@ fn process_new_zap_event( let id = zap_ctx.id; let m_promise = send_note_zap( + cache, ndb, txn, note_target, @@ -134,7 +135,7 @@ fn process_new_zap_event( ) .map(|promise| ZapPromise::FetchingInvoice { ctx: zap_ctx, - promise, + promise: Box::new(promise), }); let promise = match m_promise { @@ -151,6 +152,7 @@ fn process_new_zap_event( } fn send_note_zap( + cache: &PayCache, ndb: &Ndb, txn: &Transaction, note_target: NoteZapTargetOwned, @@ -160,15 +162,14 @@ fn send_note_zap( ) -> Result<FetchingInvoice, ZapError> { let address = get_users_zap_address(txn, ndb, &note_target.zap_recipient)?; - let promise = match address { - ZapAddress::Lud16(s) => { - fetch_invoice_lud16(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays) - } - ZapAddress::Lud06(s) => { - fetch_invoice_lnurl(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays) - } - }; - Ok(promise) + fetch_invoice_promise( + cache, + address, + msats, + *nsec, + ZapTargetOwned::Note(note_target), + relays, + ) } fn try_get_promise_response( @@ -183,7 +184,7 @@ fn try_get_promise_response( match promise { ZapPromise::FetchingInvoice { ctx, promise } => { - let result = promise.block_and_take(); + let result = Box::new(promise.block_and_take()); Some(PromiseResponse::FetchingInvoice { ctx, result }) } @@ -286,6 +287,16 @@ impl Zaps { continue; }; + if let PromiseResponse::FetchingInvoice { ctx: _, result } = &resp { + if let Ok(resp) = &**result { + if let Some(entry) = &resp.pay_entry { + let url = &entry.url; + tracing::info!("inserting {url} in pay cache"); + self.pay_cache.insert(entry.clone()); + } + } + } + self.events.push(resp.take_as_event_response()); } @@ -300,7 +311,15 @@ impl Zaps { }; let txn = nostrdb::Transaction::new(ndb).expect("txn"); - match process_event(event_resp.id, event, accounts, global_wallet, ndb, &txn) { + match process_event( + event_resp.id, + event, + &self.pay_cache, + accounts, + global_wallet, + ndb, + &txn, + ) { NextState::Event(event_resp) => { self.zaps .insert(event_resp.id, ZapState::Pending(event_resp.event)); @@ -497,7 +516,7 @@ impl std::fmt::Display for ZappingError { enum ZapPromise { FetchingInvoice { ctx: ZapCtx, - promise: Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>, + promise: Box<Promise<Result<FetchedInvoiceResponse, JoinError>>>, }, SendingNWCInvoice { ctx: SendingNWCInvoiceContext, @@ -508,7 +527,7 @@ enum ZapPromise { enum PromiseResponse { FetchingInvoice { ctx: ZapCtx, - result: Result<Result<FetchedInvoice, ZapError>, JoinError>, + result: Box<Result<FetchedInvoiceResponse, JoinError>>, }, SendingNWCInvoice { ctx: SendingNWCInvoiceContext, @@ -521,8 +540,8 @@ impl PromiseResponse { match self { PromiseResponse::FetchingInvoice { ctx, result } => { let id = ctx.id; - let event = match result { - Ok(r) => match r { + let event = match *result { + Ok(r) => match r.invoice { Ok(invoice) => Ok(ZapEvent::SendNWC { zap_ctx: ctx, req_noteid: invoice.request_noteid, diff --git a/crates/notedeck/src/zaps/networking.rs b/crates/notedeck/src/zaps/networking.rs @@ -1,4 +1,8 @@ -use crate::{error::EndpointError, zaps::ZapTargetOwned, ZapError}; +use crate::{ + error::EndpointError, + zaps::{cache::PayCache, ZapAddress, ZapTargetOwned}, + ZapError, +}; use enostr::{NoteId, Pubkey}; use nostrdb::NoteBuilder; use poll_promise::Promise; @@ -11,7 +15,12 @@ pub struct FetchedInvoice { pub request_noteid: NoteId, // note id of kind 9734 request } -pub type FetchingInvoice = Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>; +pub struct FetchedInvoiceResponse { + pub invoice: Result<FetchedInvoice, ZapError>, + pub pay_entry: Option<PayEntry>, +} + +pub type FetchingInvoice = Promise<Result<FetchedInvoiceResponse, JoinError>>; async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayResponseRaw, ZapError> { let (sender, promise) = Promise::new(); @@ -36,20 +45,9 @@ async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayResponseRaw, ZapError> tokio::task::block_in_place(|| promise.block_and_take()) } -async fn fetch_pay_req_from_lud16(lud16: &str) -> Result<LNUrlPayResponseRaw, ZapError> { - let url = match generate_endpoint_url(lud16) { - Ok(url) => url, - Err(e) => return Err(e), - }; - - fetch_pay_req_async(&url).await -} - static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl"); -fn lud16_to_lnurl(lud16: &str) -> Result<String, ZapError> { - let endpoint_url = generate_endpoint_url(lud16)?; - +fn endpoint_url_to_lnurl(endpoint_url: &Url) -> Result<String, ZapError> { let url_str = endpoint_url.to_string(); let data = url_str.as_bytes(); @@ -160,51 +158,78 @@ struct LNInvoice { invoice: String, } -fn endpoint_query_for_invoice<'a>( - endpoint_base_url: &'a mut Url, +fn endpoint_query_for_invoice( + endpoint_base_url: &Url, msats: u64, lnurl: &str, note: nostrdb::Note, -) -> Result<&'a Url, ZapError> { +) -> Result<Url, ZapError> { + let mut new_url = endpoint_base_url.clone(); let nostr = note .json() .map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?; - Ok(endpoint_base_url + new_url .query_pairs_mut() .append_pair("amount", &msats.to_string()) .append_pair("lnurl", lnurl) .append_pair("nostr", &nostr) - .finish()) -} + .finish(); -pub fn fetch_invoice_lud16( - lud16: String, - msats: u64, - sender_nsec: [u8; 32], - target: ZapTargetOwned, - relays: Vec<String>, -) -> FetchingInvoice { - Promise::spawn_async(tokio::spawn(async move { - fetch_invoice_lud16_async(&lud16, msats, &sender_nsec, target, relays).await - })) + Ok(new_url) } -pub fn fetch_invoice_lnurl( - lnurl: String, +pub fn fetch_invoice_promise( + cache: &PayCache, + zap_address: ZapAddress, msats: u64, sender_nsec: [u8; 32], target: ZapTargetOwned, relays: Vec<String>, -) -> FetchingInvoice { - Promise::spawn_async(tokio::spawn(async move { - let pay_req = match fetch_pay_req_from_lnurl_async(&lnurl).await { - Ok(req) => req, - Err(e) => return Err(e), - }; +) -> Result<FetchingInvoice, ZapError> { + let (url, lnurl) = match zap_address { + ZapAddress::Lud16(lud16) => { + let url = generate_endpoint_url(&lud16)?; + let lnurl = endpoint_url_to_lnurl(&url)?; + (url, lnurl) + } + ZapAddress::Lud06(lnurl) => (convert_lnurl_to_endpoint_url(&lnurl)?, lnurl), + }; - fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, &sender_nsec, relays, target).await - })) + match cache.get_response(&url) { + Some(endpoint_resp) => { + tracing::info!("Using existing endpoint response for {url}"); + let response = endpoint_resp.clone(); + Ok(Promise::spawn_async(tokio::spawn(async move { + fetch_invoice_lnurl_async( + &lnurl, + PayEntry { url, response }, + msats, + &sender_nsec, + relays, + target, + ) + .await + }))) + } + None => Ok(Promise::spawn_async(tokio::spawn(async move { + tracing::info!("querying ln endpoint: {url}"); + let pay_req = match fetch_pay_req_async(&url).await { + Ok(p) => PayEntry { + url, + response: p.into(), + }, + Err(e) => { + return FetchedInvoiceResponse { + invoice: Err(e), + pay_entry: None, + } + } + }; + + fetch_invoice_lnurl_async(&lnurl, pay_req, msats, &sender_nsec, relays, target).await + }))), + } } fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> { @@ -217,60 +242,51 @@ fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> { .map_err(|e| ZapError::endpoint_error(format!("endpoint url from lnurl is invalid: {e}"))) } -async fn fetch_pay_req_from_lnurl_async(lnurl: &str) -> Result<LNUrlPayResponseRaw, ZapError> { - let url = match convert_lnurl_to_endpoint_url(lnurl) { - Ok(u) => u, - Err(e) => return Err(e), - }; - - fetch_pay_req_async(&url).await -} - async fn fetch_invoice_lnurl_async( lnurl: &str, - pay_req: &LNUrlPayResponseRaw, + pay_entry: PayEntry, msats: u64, sender_nsec: &[u8; 32], relays: Vec<String>, target: ZapTargetOwned, -) -> Result<FetchedInvoice, ZapError> { - //let recipient = Pubkey::from_hex(&pay_req.nostr_pubkey) - //.map_err(|e| ZapError::EndpointError(format!("invalid pubkey hex from endpoint: {e}")))?; - - let mut base_url = Url::parse(&pay_req.callback_url).map_err(|e| { - ZapError::endpoint_error(format!("invalid callback url from endpoint: {e}")) - })?; +) -> FetchedInvoiceResponse { + let base_url = match &pay_entry.response.callback_url { + Ok(url) => url.clone(), + Err(error) => { + return FetchedInvoiceResponse { + invoice: Err(ZapError::EndpointError(error.clone())), + pay_entry: None, + }; + } + }; let (query, noteid) = { let comment: &str = ""; let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target); let noteid = NoteId::new(*note.id()); - let query = endpoint_query_for_invoice(&mut base_url, msats, lnurl, note)?; + let query = match endpoint_query_for_invoice(&base_url, msats, lnurl, note) { + Ok(u) => u, + Err(e) => { + return FetchedInvoiceResponse { + invoice: Err(e), + pay_entry: Some(pay_entry), + } + } + }; (query, noteid) }; - let res = fetch_invoice(query).await; - res.map(|i| FetchedInvoice { - invoice: i.invoice, - request_noteid: noteid, - }) -} - -async fn fetch_invoice_lud16_async( - lud16: &str, - msats: u64, - sender_nsec: &[u8; 32], - target: ZapTargetOwned, - relays: Vec<String>, -) -> Result<FetchedInvoice, ZapError> { - let pay_req = fetch_pay_req_from_lud16(lud16).await?; - - let lnurl = lud16_to_lnurl(lud16)?; - - fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, sender_nsec, relays, target).await + let res = fetch_ln_invoice(&query).await; + FetchedInvoiceResponse { + invoice: res.map(|r| FetchedInvoice { + invoice: r.invoice, + request_noteid: noteid, + }), + pay_entry: Some(pay_entry), + } } -async fn fetch_invoice(req: &Url) -> Result<LNInvoice, ZapError> { +async fn fetch_ln_invoice(req: &Url) -> Result<LNInvoice, ZapError> { let request = ehttp::Request::get(req); let (sender, promise) = Promise::new(); let on_done = move |response: Result<ehttp::Response, String>| { @@ -331,18 +347,25 @@ fn generate_endpoint_url(lud16: &str) -> Result<Url, ZapError> { mod tests { use enostr::{FullKeypair, NoteId}; - use crate::zaps::networking::convert_lnurl_to_endpoint_url; - - use super::{ - fetch_invoice_lnurl, fetch_invoice_lud16, fetch_pay_req_from_lud16, lud16_to_lnurl, + use crate::zaps::{ + cache::PayCache, + networking::{ + convert_lnurl_to_endpoint_url, endpoint_url_to_lnurl, fetch_pay_req_async, + generate_endpoint_url, + }, }; + use super::fetch_invoice_promise; + #[ignore] // don't run this test automatically since it sends real http #[tokio::test(flavor = "multi_thread")] async fn test_get_pay_req() { let lud16 = "jb55@sendsats.lol"; - let maybe_res = fetch_pay_req_from_lud16(lud16).await; + let url = generate_endpoint_url(lud16); + assert!(url.is_ok()); + + let maybe_res = fetch_pay_req_async(&url.unwrap()).await; assert!(maybe_res.is_ok()); @@ -362,7 +385,10 @@ mod tests { fn test_lnurl() { let lud16 = "jb55@sendsats.lol"; - let maybe_lnurl = lud16_to_lnurl(lud16); + let url = generate_endpoint_url(lud16); + assert!(url.is_ok()); + + let maybe_lnurl = endpoint_url_to_lnurl(&url.unwrap()); assert!(maybe_lnurl.is_ok()); let lnurl = maybe_lnurl.unwrap(); @@ -378,9 +404,11 @@ mod tests { let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); let kp = FullKeypair::generate(); + let mut cache = PayCache::default(); let maybe_invoice = rt.block_on(async { - fetch_invoice_lud16( - "jb55@sendsats.lol".to_owned(), + fetch_invoice_promise( + &mut cache, + crate::zaps::ZapAddress::Lud16("jb55@sendsats.lol".to_owned()), 1000, FullKeypair::generate().secret_key.to_secret_bytes(), crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned { @@ -389,14 +417,18 @@ mod tests { }), vec!["wss://relay.damus.io".to_owned()], ) - .block_and_take() + .map(|p| p.block_and_take()) }); assert!(maybe_invoice.is_ok()); let inner = maybe_invoice.unwrap(); assert!(inner.is_ok()); - let invoice = inner.unwrap(); - assert!(invoice.invoice.starts_with("lnbc")); + let inner = inner.unwrap().invoice; + assert!(inner.is_ok()); + + let inner = inner.unwrap(); + + assert!(inner.invoice.starts_with("lnbc")); } #[test] @@ -419,9 +451,11 @@ mod tests { let kp = FullKeypair::generate(); + let mut cache = PayCache::default(); let maybe_invoice = rt.block_on(async { - fetch_invoice_lnurl( - lnurl.to_owned(), + fetch_invoice_promise( + &mut cache, + crate::zaps::ZapAddress::Lud06(lnurl.to_owned()), 1000, kp.secret_key.to_secret_bytes(), crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned { @@ -430,7 +464,7 @@ mod tests { }), [relay.to_owned()].to_vec(), ) - .block_and_take() + .map(|p| p.block_and_take()) }); assert!(maybe_invoice.is_ok()); @@ -439,6 +473,8 @@ mod tests { let inner = inner.unwrap().invoice; assert!(inner.is_ok()); - assert!(maybe_invoice.unwrap().unwrap().invoice.starts_with("lnbc")); + let inner = inner.unwrap(); + + assert!(inner.invoice.starts_with("lnbc")); } }