use PayCache when zapping

to avoid needlessly querying ln endpoint

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2025-09-01 14:13:22 -04:00
parent 14c59a6c94
commit 5282373434
2 changed files with 171 additions and 116 deletions

View File

@@ -11,16 +11,12 @@ use crate::{
get_wallet_for, get_wallet_for,
zaps::{ zaps::{
get_users_zap_address, get_users_zap_address,
networking::{LNUrlPayResponse, PayEntry}, networking::{fetch_invoice_promise, FetchedInvoiceResponse, LNUrlPayResponse, PayEntry},
ZapAddress,
}, },
Accounts, GlobalWallet, ZapError, Accounts, GlobalWallet, ZapError,
}; };
use super::{ use super::{networking::FetchingInvoice, zap::Zap};
networking::{fetch_invoice_lnurl, fetch_invoice_lud16, FetchedInvoice, FetchingInvoice},
zap::Zap,
};
type ZapId = u32; type ZapId = u32;
@@ -34,6 +30,8 @@ pub struct Zaps {
zaps: std::collections::HashMap<ZapId, ZapState>, zaps: std::collections::HashMap<ZapId, ZapState>,
in_flight: Vec<ZapPromise>, in_flight: Vec<ZapPromise>,
events: Vec<EventResponse>, events: Vec<EventResponse>,
pay_cache: PayCache,
} }
/// Cache to hold LNURL payRequest responses from the desired LNURL endpoint /// Cache to hold LNURL payRequest responses from the desired LNURL endpoint
@@ -56,6 +54,7 @@ impl PayCache {
fn process_event( fn process_event(
id: ZapId, id: ZapId,
event: ZapEvent, event: ZapEvent,
cache: &PayCache,
accounts: &mut Accounts, accounts: &mut Accounts,
global_wallet: &mut GlobalWallet, global_wallet: &mut GlobalWallet,
ndb: &Ndb, ndb: &Ndb,
@@ -65,7 +64,7 @@ fn process_event(
ZapEvent::FetchInvoice { ZapEvent::FetchInvoice {
zap_ctx, zap_ctx,
sender_relays, 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 { ZapEvent::SendNWC {
zap_ctx, zap_ctx,
req_noteid, req_noteid,
@@ -102,6 +101,7 @@ fn process_event(
} }
fn process_new_zap_event( fn process_new_zap_event(
cache: &PayCache,
zap_ctx: ZapCtx, zap_ctx: ZapCtx,
accounts: &Accounts, accounts: &Accounts,
ndb: &Ndb, ndb: &Ndb,
@@ -125,6 +125,7 @@ fn process_new_zap_event(
let id = zap_ctx.id; let id = zap_ctx.id;
let m_promise = send_note_zap( let m_promise = send_note_zap(
cache,
ndb, ndb,
txn, txn,
note_target, note_target,
@@ -134,7 +135,7 @@ fn process_new_zap_event(
) )
.map(|promise| ZapPromise::FetchingInvoice { .map(|promise| ZapPromise::FetchingInvoice {
ctx: zap_ctx, ctx: zap_ctx,
promise, promise: Box::new(promise),
}); });
let promise = match m_promise { let promise = match m_promise {
@@ -151,6 +152,7 @@ fn process_new_zap_event(
} }
fn send_note_zap( fn send_note_zap(
cache: &PayCache,
ndb: &Ndb, ndb: &Ndb,
txn: &Transaction, txn: &Transaction,
note_target: NoteZapTargetOwned, note_target: NoteZapTargetOwned,
@@ -160,15 +162,14 @@ fn send_note_zap(
) -> Result<FetchingInvoice, ZapError> { ) -> Result<FetchingInvoice, ZapError> {
let address = get_users_zap_address(txn, ndb, &note_target.zap_recipient)?; let address = get_users_zap_address(txn, ndb, &note_target.zap_recipient)?;
let promise = match address { fetch_invoice_promise(
ZapAddress::Lud16(s) => { cache,
fetch_invoice_lud16(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays) address,
} msats,
ZapAddress::Lud06(s) => { *nsec,
fetch_invoice_lnurl(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays) ZapTargetOwned::Note(note_target),
} relays,
}; )
Ok(promise)
} }
fn try_get_promise_response( fn try_get_promise_response(
@@ -183,7 +184,7 @@ fn try_get_promise_response(
match promise { match promise {
ZapPromise::FetchingInvoice { ctx, promise } => { ZapPromise::FetchingInvoice { ctx, promise } => {
let result = promise.block_and_take(); let result = Box::new(promise.block_and_take());
Some(PromiseResponse::FetchingInvoice { ctx, result }) Some(PromiseResponse::FetchingInvoice { ctx, result })
} }
@@ -286,6 +287,16 @@ impl Zaps {
continue; 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()); self.events.push(resp.take_as_event_response());
} }
@@ -300,7 +311,15 @@ impl Zaps {
}; };
let txn = nostrdb::Transaction::new(ndb).expect("txn"); 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) => { NextState::Event(event_resp) => {
self.zaps self.zaps
.insert(event_resp.id, ZapState::Pending(event_resp.event)); .insert(event_resp.id, ZapState::Pending(event_resp.event));
@@ -497,7 +516,7 @@ impl std::fmt::Display for ZappingError {
enum ZapPromise { enum ZapPromise {
FetchingInvoice { FetchingInvoice {
ctx: ZapCtx, ctx: ZapCtx,
promise: Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>, promise: Box<Promise<Result<FetchedInvoiceResponse, JoinError>>>,
}, },
SendingNWCInvoice { SendingNWCInvoice {
ctx: SendingNWCInvoiceContext, ctx: SendingNWCInvoiceContext,
@@ -508,7 +527,7 @@ enum ZapPromise {
enum PromiseResponse { enum PromiseResponse {
FetchingInvoice { FetchingInvoice {
ctx: ZapCtx, ctx: ZapCtx,
result: Result<Result<FetchedInvoice, ZapError>, JoinError>, result: Box<Result<FetchedInvoiceResponse, JoinError>>,
}, },
SendingNWCInvoice { SendingNWCInvoice {
ctx: SendingNWCInvoiceContext, ctx: SendingNWCInvoiceContext,
@@ -521,8 +540,8 @@ impl PromiseResponse {
match self { match self {
PromiseResponse::FetchingInvoice { ctx, result } => { PromiseResponse::FetchingInvoice { ctx, result } => {
let id = ctx.id; let id = ctx.id;
let event = match result { let event = match *result {
Ok(r) => match r { Ok(r) => match r.invoice {
Ok(invoice) => Ok(ZapEvent::SendNWC { Ok(invoice) => Ok(ZapEvent::SendNWC {
zap_ctx: ctx, zap_ctx: ctx,
req_noteid: invoice.request_noteid, req_noteid: invoice.request_noteid,

View File

@@ -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 enostr::{NoteId, Pubkey};
use nostrdb::NoteBuilder; use nostrdb::NoteBuilder;
use poll_promise::Promise; use poll_promise::Promise;
@@ -11,7 +15,12 @@ pub struct FetchedInvoice {
pub request_noteid: NoteId, // note id of kind 9734 request 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> { async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayResponseRaw, ZapError> {
let (sender, promise) = Promise::new(); 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()) 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"); static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl");
fn lud16_to_lnurl(lud16: &str) -> Result<String, ZapError> { fn endpoint_url_to_lnurl(endpoint_url: &Url) -> Result<String, ZapError> {
let endpoint_url = generate_endpoint_url(lud16)?;
let url_str = endpoint_url.to_string(); let url_str = endpoint_url.to_string();
let data = url_str.as_bytes(); let data = url_str.as_bytes();
@@ -160,51 +158,78 @@ struct LNInvoice {
invoice: String, invoice: String,
} }
fn endpoint_query_for_invoice<'a>( fn endpoint_query_for_invoice(
endpoint_base_url: &'a mut Url, endpoint_base_url: &Url,
msats: u64, msats: u64,
lnurl: &str, lnurl: &str,
note: nostrdb::Note, note: nostrdb::Note,
) -> Result<&'a Url, ZapError> { ) -> Result<Url, ZapError> {
let mut new_url = endpoint_base_url.clone();
let nostr = note let nostr = note
.json() .json()
.map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?; .map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?;
Ok(endpoint_base_url new_url
.query_pairs_mut() .query_pairs_mut()
.append_pair("amount", &msats.to_string()) .append_pair("amount", &msats.to_string())
.append_pair("lnurl", lnurl) .append_pair("lnurl", lnurl)
.append_pair("nostr", &nostr) .append_pair("nostr", &nostr)
.finish()) .finish();
Ok(new_url)
} }
pub fn fetch_invoice_lud16( pub fn fetch_invoice_promise(
lud16: String, cache: &PayCache,
zap_address: ZapAddress,
msats: u64, msats: u64,
sender_nsec: [u8; 32], sender_nsec: [u8; 32],
target: ZapTargetOwned, target: ZapTargetOwned,
relays: Vec<String>, relays: Vec<String>,
) -> FetchingInvoice { ) -> Result<FetchingInvoice, ZapError> {
Promise::spawn_async(tokio::spawn(async move { let (url, lnurl) = match zap_address {
fetch_invoice_lud16_async(&lud16, msats, &sender_nsec, target, relays).await 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),
};
pub fn fetch_invoice_lnurl( match cache.get_response(&url) {
lnurl: String, Some(endpoint_resp) => {
msats: u64, tracing::info!("Using existing endpoint response for {url}");
sender_nsec: [u8; 32], let response = endpoint_resp.clone();
target: ZapTargetOwned, Ok(Promise::spawn_async(tokio::spawn(async move {
relays: Vec<String>, fetch_invoice_lnurl_async(
) -> FetchingInvoice { &lnurl,
Promise::spawn_async(tokio::spawn(async move { PayEntry { url, response },
let pay_req = match fetch_pay_req_from_lnurl_async(&lnurl).await { msats,
Ok(req) => req, &sender_nsec,
Err(e) => return Err(e), 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 fetch_invoice_lnurl_async(&lnurl, pay_req, msats, &sender_nsec, relays, target).await
})) }))),
}
} }
fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> { 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}"))) .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( async fn fetch_invoice_lnurl_async(
lnurl: &str, lnurl: &str,
pay_req: &LNUrlPayResponseRaw, pay_entry: PayEntry,
msats: u64, msats: u64,
sender_nsec: &[u8; 32], sender_nsec: &[u8; 32],
relays: Vec<String>, relays: Vec<String>,
target: ZapTargetOwned, target: ZapTargetOwned,
) -> Result<FetchedInvoice, ZapError> { ) -> FetchedInvoiceResponse {
//let recipient = Pubkey::from_hex(&pay_req.nostr_pubkey) let base_url = match &pay_entry.response.callback_url {
//.map_err(|e| ZapError::EndpointError(format!("invalid pubkey hex from endpoint: {e}")))?; Ok(url) => url.clone(),
Err(error) => {
let mut base_url = Url::parse(&pay_req.callback_url).map_err(|e| { return FetchedInvoiceResponse {
ZapError::endpoint_error(format!("invalid callback url from endpoint: {e}")) invoice: Err(ZapError::EndpointError(error.clone())),
})?; pay_entry: None,
};
}
};
let (query, noteid) = { let (query, noteid) = {
let comment: &str = ""; let comment: &str = "";
let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target); let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target);
let noteid = NoteId::new(*note.id()); 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) (query, noteid)
}; };
let res = fetch_invoice(query).await; let res = fetch_ln_invoice(&query).await;
res.map(|i| FetchedInvoice { FetchedInvoiceResponse {
invoice: i.invoice, invoice: res.map(|r| FetchedInvoice {
request_noteid: noteid, invoice: r.invoice,
}) request_noteid: noteid,
}),
pay_entry: Some(pay_entry),
}
} }
async fn fetch_invoice_lud16_async( async fn fetch_ln_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
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
}
async fn fetch_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
let request = ehttp::Request::get(req); let request = ehttp::Request::get(req);
let (sender, promise) = Promise::new(); let (sender, promise) = Promise::new();
let on_done = move |response: Result<ehttp::Response, String>| { let on_done = move |response: Result<ehttp::Response, String>| {
@@ -331,18 +347,25 @@ fn generate_endpoint_url(lud16: &str) -> Result<Url, ZapError> {
mod tests { mod tests {
use enostr::{FullKeypair, NoteId}; use enostr::{FullKeypair, NoteId};
use crate::zaps::networking::convert_lnurl_to_endpoint_url; use crate::zaps::{
cache::PayCache,
use super::{ networking::{
fetch_invoice_lnurl, fetch_invoice_lud16, fetch_pay_req_from_lud16, lud16_to_lnurl, 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 #[ignore] // don't run this test automatically since it sends real http
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_get_pay_req() { async fn test_get_pay_req() {
let lud16 = "jb55@sendsats.lol"; 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()); assert!(maybe_res.is_ok());
@@ -362,7 +385,10 @@ mod tests {
fn test_lnurl() { fn test_lnurl() {
let lud16 = "jb55@sendsats.lol"; 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()); assert!(maybe_lnurl.is_ok());
let lnurl = maybe_lnurl.unwrap(); let lnurl = maybe_lnurl.unwrap();
@@ -378,9 +404,11 @@ mod tests {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let kp = FullKeypair::generate(); let kp = FullKeypair::generate();
let mut cache = PayCache::default();
let maybe_invoice = rt.block_on(async { let maybe_invoice = rt.block_on(async {
fetch_invoice_lud16( fetch_invoice_promise(
"jb55@sendsats.lol".to_owned(), &mut cache,
crate::zaps::ZapAddress::Lud16("jb55@sendsats.lol".to_owned()),
1000, 1000,
FullKeypair::generate().secret_key.to_secret_bytes(), FullKeypair::generate().secret_key.to_secret_bytes(),
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned { crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
@@ -389,14 +417,18 @@ mod tests {
}), }),
vec!["wss://relay.damus.io".to_owned()], vec!["wss://relay.damus.io".to_owned()],
) )
.block_and_take() .map(|p| p.block_and_take())
}); });
assert!(maybe_invoice.is_ok()); assert!(maybe_invoice.is_ok());
let inner = maybe_invoice.unwrap(); let inner = maybe_invoice.unwrap();
assert!(inner.is_ok()); assert!(inner.is_ok());
let invoice = inner.unwrap(); let inner = inner.unwrap().invoice;
assert!(invoice.invoice.starts_with("lnbc")); assert!(inner.is_ok());
let inner = inner.unwrap();
assert!(inner.invoice.starts_with("lnbc"));
} }
#[test] #[test]
@@ -419,9 +451,11 @@ mod tests {
let kp = FullKeypair::generate(); let kp = FullKeypair::generate();
let mut cache = PayCache::default();
let maybe_invoice = rt.block_on(async { let maybe_invoice = rt.block_on(async {
fetch_invoice_lnurl( fetch_invoice_promise(
lnurl.to_owned(), &mut cache,
crate::zaps::ZapAddress::Lud06(lnurl.to_owned()),
1000, 1000,
kp.secret_key.to_secret_bytes(), kp.secret_key.to_secret_bytes(),
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned { crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
@@ -430,7 +464,7 @@ mod tests {
}), }),
[relay.to_owned()].to_vec(), [relay.to_owned()].to_vec(),
) )
.block_and_take() .map(|p| p.block_and_take())
}); });
assert!(maybe_invoice.is_ok()); assert!(maybe_invoice.is_ok());
@@ -439,6 +473,8 @@ mod tests {
let inner = inner.unwrap().invoice; let inner = inner.unwrap().invoice;
assert!(inner.is_ok()); assert!(inner.is_ok());
assert!(maybe_invoice.unwrap().unwrap().invoice.starts_with("lnbc")); let inner = inner.unwrap();
assert!(inner.invoice.starts_with("lnbc"));
} }
} }