diff --git a/mutiny-core/src/labels.rs b/mutiny-core/src/labels.rs index 3869f2959..493924e21 100644 --- a/mutiny-core/src/labels.rs +++ b/mutiny-core/src/labels.rs @@ -22,7 +22,7 @@ pub struct LabelItem { /// List of addresses that have this label pub addresses: HashSet, /// List of invoices that have this label - pub invoices: Vec, + pub invoices: HashSet, /// Epoch time in seconds when this label was last used pub last_used_time: u64, } @@ -234,7 +234,7 @@ impl LabelStorage for S { // Create a new label item let label_item = LabelItem { addresses, - invoices: vec![], + invoices: HashSet::new(), last_used_time: now, }; self.set_data(key, label_item, None)?; @@ -263,9 +263,7 @@ impl LabelStorage for S { Some(mut label_item) => { // Add the invoice to the label item // and sort so we can dedup the invoices - label_item.invoices.push(invoice.clone()); - label_item.invoices.sort(); - label_item.invoices.dedup(); + label_item.invoices.insert(invoice.clone()); // Update the last used timestamp label_item.last_used_time = now; @@ -281,9 +279,10 @@ impl LabelStorage for S { } None => { // Create a new label item + let invoices = HashSet::from_iter(vec![invoice.clone()]); let label_item = LabelItem { addresses: HashSet::new(), - invoices: vec![invoice.clone()], + invoices, last_used_time: now, }; self.set_data(key, label_item, None)?; @@ -574,7 +573,7 @@ mod tests { "test2".to_string(), LabelItem { addresses: HashSet::from_iter(vec!["1BitcoinEaterAddressDontSendf59kuE".to_string()]), - invoices: vec![Bolt11Invoice::from_str("lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm").unwrap()], + invoices: HashSet::from_iter(vec![Bolt11Invoice::from_str("lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm").unwrap()]), ..Default::default() }, ); @@ -888,7 +887,15 @@ mod tests { let label_item = storage.get_label(&new_label).unwrap(); assert!(label_item.is_some()); - assert_eq!(label_item.clone().unwrap().invoices, vec![invoice]); + assert_eq!( + label_item + .clone() + .unwrap() + .invoices + .into_iter() + .collect_vec(), + vec![invoice] + ); assert_eq!( label_item.unwrap().addresses.into_iter().collect_vec(), vec![address.to_string()] diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 440fb32b9..17d2d5d8d 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -47,9 +47,10 @@ pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; use crate::storage::{ - list_payment_info, persist_payment_info, update_nostr_contact_list, IndexItem, MutinyStorage, - DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, - PAYMENT_INBOUND_PREFIX_KEY, PAYMENT_OUTBOUND_PREFIX_KEY, SUBSCRIPTION_TIMESTAMP, + get_payment_hash_from_key, list_payment_info, persist_payment_info, update_nostr_contact_list, + IndexItem, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY, + ONCHAIN_PREFIX, PAYMENT_INBOUND_PREFIX_KEY, PAYMENT_OUTBOUND_PREFIX_KEY, + SUBSCRIPTION_TIMESTAMP, }; use crate::{auth::MutinyAuthClient, hermes::HermesClient, logging::MutinyLogger}; use crate::{blindauth::BlindAuthClient, cashu::CashuHttpClient}; @@ -114,6 +115,7 @@ use nostr_sdk::{NostrSigner, RelayPoolNotification}; use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashSet; use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; @@ -1720,10 +1722,7 @@ impl MutinyWallet { true => PAYMENT_INBOUND_PREFIX_KEY, false => PAYMENT_OUTBOUND_PREFIX_KEY, }; - let payment_hash_str = key - .trim_start_matches(prefix) - .splitn(2, '_') // To support the old format that had `_{node_id}` at the end - .collect::>()[0]; + let payment_hash_str = get_payment_hash_from_key(key, prefix); let hash: [u8; 32] = FromHex::from_hex(payment_hash_str)?; return MutinyInvoice::from(info, PaymentHash(hash), inbound, labels).map(Some); @@ -1810,6 +1809,59 @@ impl MutinyWallet { Ok(activities) } + /// Returns all the lightning activity for a given label + pub async fn get_label_activity( + &self, + label: &String, + ) -> Result, MutinyError> { + let Some(label_item) = self.node_manager.get_label(label)? else { + return Ok(Vec::new()); + }; + + // get all the payment hashes for this label + let payment_hashes: HashSet = label_item + .invoices + .into_iter() + .map(|i| *i.payment_hash()) + .collect(); + + let index = self.storage.activity_index(); + let index = index.try_read()?.clone().into_iter().collect_vec(); + + let labels_map = self.storage.get_invoice_labels()?; + + let mut activities = Vec::with_capacity(index.len()); + for item in index { + if item.key.starts_with(PAYMENT_INBOUND_PREFIX_KEY) { + let payment_hash_str = + get_payment_hash_from_key(&item.key, PAYMENT_INBOUND_PREFIX_KEY); + let hash = sha256::Hash::from_str(payment_hash_str)?; + + if payment_hashes.contains(&hash) { + if let Some(mutiny_invoice) = + self.get_invoice_internal(&item.key, true, &labels_map)? + { + activities.push(ActivityItem::Lightning(Box::new(mutiny_invoice))); + } + } + } else if item.key.starts_with(PAYMENT_OUTBOUND_PREFIX_KEY) { + let payment_hash_str = + get_payment_hash_from_key(&item.key, PAYMENT_OUTBOUND_PREFIX_KEY); + let hash = sha256::Hash::from_str(payment_hash_str)?; + + if payment_hashes.contains(&hash) { + if let Some(mutiny_invoice) = + self.get_invoice_internal(&item.key, true, &labels_map)? + { + activities.push(ActivityItem::Lightning(Box::new(mutiny_invoice))); + } + } + } + } + + Ok(activities) + } + pub fn list_invoices(&self) -> Result, MutinyError> { let mut inbound_invoices = self.list_payment_info_from_persisters(true)?; let mut outbound_invoices = self.list_payment_info_from_persisters(false)?; diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index 51a77fc22..3ae8b8aca 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -2725,6 +2725,7 @@ mod wasm_test { use crate::test_utils::create_node; use crate::{error::MutinyError, storage::persist_payment_info}; use crate::{HTLCStatus, PrivacyLevel}; + use itertools::Itertools; use lightning::ln::channelmanager::PaymentId; use lightning::ln::PaymentHash; use lightning_invoice::Bolt11InvoiceDescription; @@ -2789,7 +2790,7 @@ mod wasm_test { assert!(label_item.last_used_time >= now); assert!(label_item.addresses.is_empty()); - assert_eq!(label_item.invoices, vec![invoice]); + assert_eq!(label_item.invoices.into_iter().collect_vec(), vec![invoice]); } #[test] diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 98f480c3f..ad272b768 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1,10 +1,8 @@ use crate::auth::MutinyAuthClient; -use crate::event::HTLCStatus; use crate::labels::LabelStorage; use crate::ldkstorage::CHANNEL_CLOSURE_PREFIX; use crate::logging::LOGGING_KEY; use crate::utils::{sleep, spawn}; -use crate::ActivityItem; use crate::MutinyInvoice; use crate::MutinyWalletConfig; use crate::{ @@ -966,47 +964,6 @@ impl NodeManager { Ok(details_opt.map(|(d, _)| d)) } - /// Returns all the on-chain and lightning activity for a given label - pub async fn get_label_activity( - &self, - label: &String, - ) -> Result, MutinyError> { - let Some(label_item) = self.get_label(label)? else { - return Ok(Vec::new()); - }; - - let mut activity = vec![]; - for inv in label_item.invoices.iter() { - if let Ok(ln) = self.get_invoice_by_hash(inv.payment_hash()).await { - // Only show paid and in-flight invoices - match ln.status { - HTLCStatus::Succeeded | HTLCStatus::InFlight => { - activity.push(ActivityItem::Lightning(Box::new(ln))); - } - HTLCStatus::Pending | HTLCStatus::Failed => {} - } - } - } - let onchain = self - .list_onchain() - .map_err(|e| { - log_warn!(self.logger, "Failed to get bdk history: {e}"); - e - }) - .unwrap_or_default(); - - for on in onchain { - if on.labels.contains(label) { - activity.push(ActivityItem::OnChain(on)); - } - } - - // Newest first - activity.sort_by(|a, b| b.cmp(a)); - - Ok(activity) - } - /// Adds labels to the TransactionDetails based on the address labels. /// This will panic if the TransactionDetails does not have a transaction. /// Make sure you flag `include_raw` when calling `list_transactions` to @@ -2113,10 +2070,8 @@ pub fn create_lsp_config( mod tests { use crate::{ encrypt::encryption_key_from_pass, - nodemanager::{ - ActivityItem, ChannelClosure, MutinyInvoice, NodeManager, TransactionDetails, - }, - MutinyWalletConfigBuilder, PrivacyLevel, + nodemanager::{ChannelClosure, MutinyInvoice, NodeManager, TransactionDetails}, + ActivityItem, MutinyWalletConfigBuilder, PrivacyLevel, }; use crate::{keymanager::generate_seed, nodemanager::NodeManagerBuilder}; use bdk::chain::ConfirmationTime; diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 8a76c238b..80531ba2c 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -986,10 +986,7 @@ pub(crate) fn list_payment_info( Ok(map .into_iter() .map(|(key, value)| { - let payment_hash_str = key - .trim_start_matches(prefix) - .splitn(2, '_') // To support the old format that had `_{node_id}` at the end - .collect::>()[0]; + let payment_hash_str = get_payment_hash_from_key(key.as_str(), prefix); let hash: [u8; 32] = FromHex::from_hex(payment_hash_str).expect("key should be a sha256 hash"); (PaymentHash(hash), value) @@ -1057,6 +1054,12 @@ where } } +pub(crate) fn get_payment_hash_from_key<'a>(key: &'a str, prefix: &str) -> &'a str { + key.trim_start_matches(prefix) + .splitn(2, '_') // To support the old format that had `_{node_id}` at the end + .collect::>()[0] +} + #[cfg(test)] mod tests { use crate::test_utils::*; diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 16d3d7f1b..bcd46658f 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1180,7 +1180,7 @@ impl MutinyWallet { label: String, ) -> Result */, MutinyJsError> { // get activity from the node manager - let activity = self.inner.node_manager.get_label_activity(&label).await?; + let activity = self.inner.get_label_activity(&label).await?; let mut activity: Vec = activity.into_iter().map(|a| a.into()).collect(); // add contact to the activity item it has one, otherwise return the activity list