Skip to content

Commit

Permalink
Merge pull request #1089 from MutinyWallet/rm-pending-nwc
Browse files Browse the repository at this point in the history
Better handling for marking DM'd invoices as paid
  • Loading branch information
benthecarman committed Mar 26, 2024
2 parents dd3de02 + f45df05 commit 4c81907
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 28 deletions.
32 changes: 28 additions & 4 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,8 +1156,8 @@ impl<S: MutinyStorage> MutinyWallet<S> {
log_warn!(logger, "Failed to clear in-active NWC profiles: {e}");
}

if let Err(e) = nostr.clear_expired_nwc_invoices().await {
log_warn!(logger, "Failed to clear expired NWC invoices: {e}");
if let Err(e) = nostr.clear_invalid_nwc_invoices(&self_clone).await {
log_warn!(logger, "Failed to clear invalid NWC invoices: {e}");
}

let client = Client::new(nostr.primary_key.clone());
Expand Down Expand Up @@ -1331,6 +1331,17 @@ impl<S: MutinyStorage> MutinyWallet<S> {
.await;
match payment_result {
Ok(r) => {
// spawn a task to remove the pending invoice if it exists
let nostr_clone = self.nostr.clone();
let payment_hash = *inv.payment_hash();
let logger = self.logger.clone();
utils::spawn(async move {
if let Err(e) =
nostr_clone.remove_pending_nwc_invoice(&payment_hash).await
{
log_warn!(logger, "Failed to remove pending NWC invoice: {e}");
}
});
return Ok(r);
}
Err(e) => match e {
Expand Down Expand Up @@ -1368,9 +1379,22 @@ impl<S: MutinyStorage> MutinyWallet<S> {
.sum::<u64>()
> 0
{
self.node_manager
let res = self
.node_manager
.pay_invoice(None, inv, amt_sats, labels)
.await
.await?;

// spawn a task to remove the pending invoice if it exists
let nostr_clone = self.nostr.clone();
let payment_hash = *inv.payment_hash();
let logger = self.logger.clone();
utils::spawn(async move {
if let Err(e) = nostr_clone.remove_pending_nwc_invoice(&payment_hash).await {
log_warn!(logger, "Failed to remove pending NWC invoice: {e}");
}
});

Ok(res)
} else {
Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance))
}
Expand Down
82 changes: 61 additions & 21 deletions mutiny-core/src/nostr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,35 @@ impl<S: MutinyStorage> NostrManager<S> {
Ok(event_id)
}

/// Removes an invoice from the pending list
pub(crate) async fn remove_pending_nwc_invoice(
&self,
hash: &sha256::Hash,
) -> Result<(), MutinyError> {
// get lock for writing
self.pending_nwc_lock.lock().await;

let mut pending: Vec<PendingNwcInvoice> = self
.storage
.get_data(PENDING_NWC_EVENTS_KEY)?
.unwrap_or_default();

let original_len = pending.len();

// remove from storage
pending.retain(|x| x.invoice.payment_hash() != hash);

// if we didn't remove anything, we don't need to save
if original_len == pending.len() {
return Ok(());
}

self.storage
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), pending, None)?;

Ok(())
}

/// Approves an invoice and sends the payment
pub async fn approve_invoice(
&self,
Expand Down Expand Up @@ -998,19 +1027,8 @@ impl<S: MutinyStorage> NostrManager<S> {
}
};

// get lock for writing
self.pending_nwc_lock.lock().await;

// get from storage again, in case it was updated
let mut pending: Vec<PendingNwcInvoice> = self
.storage
.get_data(PENDING_NWC_EVENTS_KEY)?
.unwrap_or_default();

// remove from storage
pending.retain(|x| x.invoice.payment_hash() != &hash);
self.storage
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), pending, None)?;
// remove from our pending list
self.remove_pending_nwc_invoice(&hash).await?;

Ok(event_id)
}
Expand Down Expand Up @@ -1112,23 +1130,45 @@ impl<S: MutinyStorage> NostrManager<S> {
Ok(())
}

/// Goes through all pending NWC invoices and removes the expired ones
pub async fn clear_expired_nwc_invoices(&self) -> Result<(), MutinyError> {
/// Goes through all pending NWC invoices and removes invoices that are
/// expired or have been paid
pub async fn clear_invalid_nwc_invoices(
&self,
invoice_handler: &impl InvoiceHandler,
) -> Result<(), MutinyError> {
self.pending_nwc_lock.lock().await;
let mut invoices: Vec<PendingNwcInvoice> = self
let invoices: Vec<PendingNwcInvoice> = self
.storage
.get_data(PENDING_NWC_EVENTS_KEY)?
.unwrap_or_default();

// remove expired invoices
invoices.retain(|x| !x.is_expired());
let mut new_invoices = Vec::with_capacity(invoices.len());

for inv in invoices {
// remove expired invoices
if inv.is_expired() {
continue;
}

// remove paid invoices
if invoice_handler
.lookup_payment(&inv.invoice.payment_hash().into_32())
.await
.is_some_and(|p| p.status == HTLCStatus::Succeeded)
{
continue;
}

// keep the invoice if it is still valid
new_invoices.push(inv);
}

// sort and dedup
invoices.sort();
invoices.dedup();
new_invoices.sort();
new_invoices.dedup();

self.storage
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), invoices, None)?;
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), new_invoices, None)?;

Ok(())
}
Expand Down
28 changes: 25 additions & 3 deletions mutiny-core/src/nostr/nwc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1795,7 +1795,7 @@ mod wasm_test {
}

#[test]
async fn test_clear_expired_pending_invoices() {
async fn test_clear_invalid_pending_invoices() {
let storage = MemoryStorage::default();
let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap();
let stop = Arc::new(AtomicBool::new(false));
Expand All @@ -1822,9 +1822,10 @@ mod wasm_test {
identifier: None,
};
// add an unexpired invoice
let dummy_invoice = create_dummy_invoice(Some(1_000), Network::Regtest, None).0;
let unexpired = PendingNwcInvoice {
index: Some(0),
invoice: create_dummy_invoice(Some(1_000), Network::Regtest, None).0,
invoice: dummy_invoice.clone(),
event_id: EventId::all_zeros(),
pubkey: nostr_manager.public_key,
identifier: None,
Expand All @@ -1841,10 +1842,31 @@ mod wasm_test {
assert_eq!(pending.len(), 2);

// check that the expired invoice is cleared
nostr_manager.clear_expired_nwc_invoices().await.unwrap();
let mut node = MockInvoiceHandler::new();
node.expect_lookup_payment().return_const(None);
nostr_manager
.clear_invalid_nwc_invoices(&node)
.await
.unwrap();
let pending = nostr_manager.get_pending_nwc_invoices().unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0], unexpired);

let mut node = MockInvoiceHandler::new();
node.expect_lookup_payment()
.with(eq(dummy_invoice.payment_hash().into_32()))
.returning(move |_| {
Some(MutinyInvoice {
status: HTLCStatus::Succeeded,
..Default::default()
})
});
nostr_manager
.clear_invalid_nwc_invoices(&node)
.await
.unwrap();
let pending = nostr_manager.get_pending_nwc_invoices().unwrap();
assert!(pending.is_empty());
}

#[test]
Expand Down

0 comments on commit 4c81907

Please sign in to comment.