Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit 4c81907

Browse files
authored
Merge pull request #1089 from MutinyWallet/rm-pending-nwc
Better handling for marking DM'd invoices as paid
2 parents dd3de02 + f45df05 commit 4c81907

3 files changed

Lines changed: 114 additions & 28 deletions

File tree

mutiny-core/src/lib.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,8 +1156,8 @@ impl<S: MutinyStorage> MutinyWallet<S> {
11561156
log_warn!(logger, "Failed to clear in-active NWC profiles: {e}");
11571157
}
11581158

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

11631163
let client = Client::new(nostr.primary_key.clone());
@@ -1331,6 +1331,17 @@ impl<S: MutinyStorage> MutinyWallet<S> {
13311331
.await;
13321332
match payment_result {
13331333
Ok(r) => {
1334+
// spawn a task to remove the pending invoice if it exists
1335+
let nostr_clone = self.nostr.clone();
1336+
let payment_hash = *inv.payment_hash();
1337+
let logger = self.logger.clone();
1338+
utils::spawn(async move {
1339+
if let Err(e) =
1340+
nostr_clone.remove_pending_nwc_invoice(&payment_hash).await
1341+
{
1342+
log_warn!(logger, "Failed to remove pending NWC invoice: {e}");
1343+
}
1344+
});
13341345
return Ok(r);
13351346
}
13361347
Err(e) => match e {
@@ -1368,9 +1379,22 @@ impl<S: MutinyStorage> MutinyWallet<S> {
13681379
.sum::<u64>()
13691380
> 0
13701381
{
1371-
self.node_manager
1382+
let res = self
1383+
.node_manager
13721384
.pay_invoice(None, inv, amt_sats, labels)
1373-
.await
1385+
.await?;
1386+
1387+
// spawn a task to remove the pending invoice if it exists
1388+
let nostr_clone = self.nostr.clone();
1389+
let payment_hash = *inv.payment_hash();
1390+
let logger = self.logger.clone();
1391+
utils::spawn(async move {
1392+
if let Err(e) = nostr_clone.remove_pending_nwc_invoice(&payment_hash).await {
1393+
log_warn!(logger, "Failed to remove pending NWC invoice: {e}");
1394+
}
1395+
});
1396+
1397+
Ok(res)
13741398
} else {
13751399
Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance))
13761400
}

mutiny-core/src/nostr/mod.rs

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,35 @@ impl<S: MutinyStorage> NostrManager<S> {
966966
Ok(event_id)
967967
}
968968

969+
/// Removes an invoice from the pending list
970+
pub(crate) async fn remove_pending_nwc_invoice(
971+
&self,
972+
hash: &sha256::Hash,
973+
) -> Result<(), MutinyError> {
974+
// get lock for writing
975+
self.pending_nwc_lock.lock().await;
976+
977+
let mut pending: Vec<PendingNwcInvoice> = self
978+
.storage
979+
.get_data(PENDING_NWC_EVENTS_KEY)?
980+
.unwrap_or_default();
981+
982+
let original_len = pending.len();
983+
984+
// remove from storage
985+
pending.retain(|x| x.invoice.payment_hash() != hash);
986+
987+
// if we didn't remove anything, we don't need to save
988+
if original_len == pending.len() {
989+
return Ok(());
990+
}
991+
992+
self.storage
993+
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), pending, None)?;
994+
995+
Ok(())
996+
}
997+
969998
/// Approves an invoice and sends the payment
970999
pub async fn approve_invoice(
9711000
&self,
@@ -998,19 +1027,8 @@ impl<S: MutinyStorage> NostrManager<S> {
9981027
}
9991028
};
10001029

1001-
// get lock for writing
1002-
self.pending_nwc_lock.lock().await;
1003-
1004-
// get from storage again, in case it was updated
1005-
let mut pending: Vec<PendingNwcInvoice> = self
1006-
.storage
1007-
.get_data(PENDING_NWC_EVENTS_KEY)?
1008-
.unwrap_or_default();
1009-
1010-
// remove from storage
1011-
pending.retain(|x| x.invoice.payment_hash() != &hash);
1012-
self.storage
1013-
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), pending, None)?;
1030+
// remove from our pending list
1031+
self.remove_pending_nwc_invoice(&hash).await?;
10141032

10151033
Ok(event_id)
10161034
}
@@ -1112,23 +1130,45 @@ impl<S: MutinyStorage> NostrManager<S> {
11121130
Ok(())
11131131
}
11141132

1115-
/// Goes through all pending NWC invoices and removes the expired ones
1116-
pub async fn clear_expired_nwc_invoices(&self) -> Result<(), MutinyError> {
1133+
/// Goes through all pending NWC invoices and removes invoices that are
1134+
/// expired or have been paid
1135+
pub async fn clear_invalid_nwc_invoices(
1136+
&self,
1137+
invoice_handler: &impl InvoiceHandler,
1138+
) -> Result<(), MutinyError> {
11171139
self.pending_nwc_lock.lock().await;
1118-
let mut invoices: Vec<PendingNwcInvoice> = self
1140+
let invoices: Vec<PendingNwcInvoice> = self
11191141
.storage
11201142
.get_data(PENDING_NWC_EVENTS_KEY)?
11211143
.unwrap_or_default();
11221144

1123-
// remove expired invoices
1124-
invoices.retain(|x| !x.is_expired());
1145+
let mut new_invoices = Vec::with_capacity(invoices.len());
1146+
1147+
for inv in invoices {
1148+
// remove expired invoices
1149+
if inv.is_expired() {
1150+
continue;
1151+
}
1152+
1153+
// remove paid invoices
1154+
if invoice_handler
1155+
.lookup_payment(&inv.invoice.payment_hash().into_32())
1156+
.await
1157+
.is_some_and(|p| p.status == HTLCStatus::Succeeded)
1158+
{
1159+
continue;
1160+
}
1161+
1162+
// keep the invoice if it is still valid
1163+
new_invoices.push(inv);
1164+
}
11251165

11261166
// sort and dedup
1127-
invoices.sort();
1128-
invoices.dedup();
1167+
new_invoices.sort();
1168+
new_invoices.dedup();
11291169

11301170
self.storage
1131-
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), invoices, None)?;
1171+
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), new_invoices, None)?;
11321172

11331173
Ok(())
11341174
}

mutiny-core/src/nostr/nwc.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,7 +1795,7 @@ mod wasm_test {
17951795
}
17961796

17971797
#[test]
1798-
async fn test_clear_expired_pending_invoices() {
1798+
async fn test_clear_invalid_pending_invoices() {
17991799
let storage = MemoryStorage::default();
18001800
let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap();
18011801
let stop = Arc::new(AtomicBool::new(false));
@@ -1822,9 +1822,10 @@ mod wasm_test {
18221822
identifier: None,
18231823
};
18241824
// add an unexpired invoice
1825+
let dummy_invoice = create_dummy_invoice(Some(1_000), Network::Regtest, None).0;
18251826
let unexpired = PendingNwcInvoice {
18261827
index: Some(0),
1827-
invoice: create_dummy_invoice(Some(1_000), Network::Regtest, None).0,
1828+
invoice: dummy_invoice.clone(),
18281829
event_id: EventId::all_zeros(),
18291830
pubkey: nostr_manager.public_key,
18301831
identifier: None,
@@ -1841,10 +1842,31 @@ mod wasm_test {
18411842
assert_eq!(pending.len(), 2);
18421843

18431844
// check that the expired invoice is cleared
1844-
nostr_manager.clear_expired_nwc_invoices().await.unwrap();
1845+
let mut node = MockInvoiceHandler::new();
1846+
node.expect_lookup_payment().return_const(None);
1847+
nostr_manager
1848+
.clear_invalid_nwc_invoices(&node)
1849+
.await
1850+
.unwrap();
18451851
let pending = nostr_manager.get_pending_nwc_invoices().unwrap();
18461852
assert_eq!(pending.len(), 1);
18471853
assert_eq!(pending[0], unexpired);
1854+
1855+
let mut node = MockInvoiceHandler::new();
1856+
node.expect_lookup_payment()
1857+
.with(eq(dummy_invoice.payment_hash().into_32()))
1858+
.returning(move |_| {
1859+
Some(MutinyInvoice {
1860+
status: HTLCStatus::Succeeded,
1861+
..Default::default()
1862+
})
1863+
});
1864+
nostr_manager
1865+
.clear_invalid_nwc_invoices(&node)
1866+
.await
1867+
.unwrap();
1868+
let pending = nostr_manager.get_pending_nwc_invoices().unwrap();
1869+
assert!(pending.is_empty());
18481870
}
18491871

18501872
#[test]

0 commit comments

Comments
 (0)