From 8df6bc9e3c64ccf663ed517c15087bb3ed90fbb7 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Fri, 19 Jan 2024 04:31:22 -0600 Subject: [PATCH] Refactor listing invoices --- mutiny-core/src/federation.rs | 3 +- mutiny-core/src/lib.rs | 229 +++++++++++++++++++++++++++++++-- mutiny-core/src/node.rs | 40 +----- mutiny-core/src/nodemanager.rs | 206 ++--------------------------- mutiny-core/src/nostr/nwc.rs | 2 +- mutiny-wasm/src/lib.rs | 4 +- mutiny-wasm/src/models.rs | 5 +- 7 files changed, 241 insertions(+), 248 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 6197061d2..b727443c7 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -3,13 +3,12 @@ use crate::{ event::PaymentInfo, key::{create_root_child_key, ChildKey}, logging::MutinyLogger, - nodemanager::MutinyInvoice, onchain::coin_type_from_network, storage::{ get_payment_info, list_payment_info, persist_payment_info, MutinyStorage, VersionedValue, }, utils::sleep, - HTLCStatus, DEFAULT_PAYMENT_TIMEOUT, + HTLCStatus, MutinyInvoice, DEFAULT_PAYMENT_TIMEOUT, }; use async_trait::async_trait; use bip39::Mnemonic; diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 62943aa27..2179f8c34 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -13,7 +13,7 @@ pub mod auth; mod chain; pub mod encrypt; pub mod error; -mod event; +pub mod event; pub mod federation; mod fees; mod gossip; @@ -40,12 +40,13 @@ pub mod vss; #[cfg(test)] mod test_utils; -pub use crate::event::HTLCStatus; +use crate::event::{HTLCStatus, MillisatAmount, PaymentInfo}; 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_MANAGER_KEY, MONITORS_PREFIX_KEY}; - -use crate::storage::{MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY}; +use crate::storage::{ + list_payment_info, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY, +}; use crate::{auth::MutinyAuthClient, logging::MutinyLogger}; use crate::{error::MutinyError, nostr::ReservedProfile}; use crate::{ @@ -55,7 +56,7 @@ use crate::{ }; use crate::{ lnurlauth::make_lnurl_auth_connection, - nodemanager::{ChannelClosure, MutinyBip21RawMaterials, MutinyInvoice, TransactionDetails}, + nodemanager::{ChannelClosure, MutinyBip21RawMaterials, TransactionDetails}, }; use crate::{lnurlauth::AuthManager, nostr::MUTINY_PLUS_SUBSCRIPTION_LABEL}; use crate::{logging::LOGGING_KEY, nodemanager::NodeManagerBuilder}; @@ -73,13 +74,16 @@ use async_lock::RwLock; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; use bitcoin::hashes::hex::ToHex; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::PublicKey; use bitcoin::util::bip32::ExtendedPrivKey; -use bitcoin::{hashes::sha256, Network}; -use fedimint_core::{api::InviteCode, config::FederationId, BitcoinHash}; +use bitcoin::Network; +use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; +use lightning::ln::PaymentHash; use lightning::{log_debug, util::logger::Logger}; use lightning::{log_error, log_info, log_warn}; -use lightning_invoice::Bolt11Invoice; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use lnurl::{lnurl::LnUrl, AsyncClient as LnUrlClient, LnUrlResponse, Response}; use nostr_sdk::{Client, RelayPoolNotification}; use serde::{Deserialize, Serialize}; @@ -250,6 +254,155 @@ impl Ord for ActivityItem { } } +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct MutinyInvoice { + pub bolt11: Option, + pub description: Option, + pub payment_hash: sha256::Hash, + pub preimage: Option, + pub payee_pubkey: Option, + pub amount_sats: Option, + pub expire: u64, + pub status: HTLCStatus, + pub fees_paid: Option, + pub inbound: bool, + pub labels: Vec, + pub last_updated: u64, +} + +impl MutinyInvoice { + pub fn paid(&self) -> bool { + self.status == HTLCStatus::Succeeded + } +} + +impl From for MutinyInvoice { + fn from(value: Bolt11Invoice) -> Self { + let description = match value.description() { + Bolt11InvoiceDescription::Direct(a) => { + if a.is_empty() { + None + } else { + Some(a.to_string()) + } + } + Bolt11InvoiceDescription::Hash(_) => None, + }; + + let timestamp = value.duration_since_epoch().as_secs(); + let expiry = timestamp + value.expiry_time().as_secs(); + + let payment_hash = value.payment_hash().to_owned(); + let payee_pubkey = value.payee_pub_key().map(|p| p.to_owned()); + let amount_sats = value.amount_milli_satoshis().map(|m| m / 1000); + + MutinyInvoice { + bolt11: Some(value), + description, + payment_hash, + preimage: None, + payee_pubkey, + amount_sats, + expire: expiry, + status: HTLCStatus::Pending, + fees_paid: None, + inbound: true, + labels: vec![], + last_updated: timestamp, + } + } +} + +impl From for PaymentInfo { + fn from(invoice: MutinyInvoice) -> Self { + let preimage = invoice + .preimage + .map(|s| hex::decode(s).expect("preimage should decode")) + .map(|v| { + let mut arr = [0; 32]; + arr[..].copy_from_slice(&v); + arr + }); + let secret = None; + let status = invoice.status; + let amt_msat = invoice + .amount_sats + .map(|s| MillisatAmount(Some(s))) + .unwrap_or(MillisatAmount(None)); + let fee_paid_msat = invoice.fees_paid; + let bolt11 = invoice.bolt11; + let payee_pubkey = invoice.payee_pubkey; + let last_update = invoice.last_updated; + + PaymentInfo { + preimage, + secret, + status, + amt_msat, + fee_paid_msat, + bolt11, + payee_pubkey, + last_update, + } + } +} + +impl MutinyInvoice { + pub(crate) fn from( + i: PaymentInfo, + payment_hash: PaymentHash, + inbound: bool, + labels: Vec, + ) -> Result { + match i.bolt11 { + Some(invoice) => { + // Construct an invoice from a bolt11, easy + let amount_sats = if let Some(inv_amt) = invoice.amount_milli_satoshis() { + if inv_amt == 0 { + i.amt_msat.0.map(|a| a / 1_000) + } else { + Some(inv_amt / 1_000) + } + } else { + i.amt_msat.0.map(|a| a / 1_000) + }; + Ok(MutinyInvoice { + inbound, + last_updated: i.last_update, + status: i.status, + labels, + amount_sats, + payee_pubkey: i.payee_pubkey, + preimage: i.preimage.map(|p| p.to_hex()), + fees_paid: i.fee_paid_msat.map(|f| f / 1_000), + ..invoice.into() + }) + } + None => { + let amount_sats: Option = i.amt_msat.0.map(|s| s / 1_000); + let fees_paid = i.fee_paid_msat.map(|f| f / 1_000); + let preimage = i.preimage.map(|p| p.to_hex()); + let payment_hash = sha256::Hash::from_inner(payment_hash.0); + let invoice = MutinyInvoice { + bolt11: None, + description: None, + payment_hash, + preimage, + payee_pubkey: i.payee_pubkey, + amount_sats, + expire: i.last_update, + status: i.status, + fees_paid, + inbound, + labels, + last_updated: i.last_update, + }; + Ok(invoice) + } + } + } +} + pub struct MutinyWalletConfigBuilder { xprivkey: ExtendedPrivKey, #[cfg(target_arch = "wasm32")] @@ -952,8 +1105,34 @@ impl MutinyWallet { /// Get the sorted activity list for lightning payments, channels, and txs. pub async fn get_activity(&self) -> Result, MutinyError> { + // Get activity for lightning invoices + let lightning = self + .list_invoices() + .map_err(|e| { + log_warn!(self.logger, "Failed to get lightning activity: {e}"); + e + }) + .unwrap_or_default(); + // Get activities from node manager - let mut activities = self.node_manager.get_activity().await?; + let (closures, onchain) = self.node_manager.get_activity().await?; + + let mut activities = Vec::with_capacity(lightning.len() + onchain.len() + closures.len()); + for ln in lightning { + // Only show paid and in-flight invoices + match ln.status { + HTLCStatus::Succeeded | HTLCStatus::InFlight => { + activities.push(ActivityItem::Lightning(Box::new(ln))); + } + HTLCStatus::Pending | HTLCStatus::Failed => {} + } + } + for on in onchain { + activities.push(ActivityItem::OnChain(on)); + } + for chan in closures { + activities.push(ActivityItem::ChannelClosed(chan)); + } // Sort all activities, newest first activities.sort_by(|a, b| b.cmp(a)); @@ -961,6 +1140,38 @@ impl MutinyWallet { 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)?; + inbound_invoices.append(&mut outbound_invoices); + Ok(inbound_invoices) + } + + fn list_payment_info_from_persisters( + &self, + inbound: bool, + ) -> Result, MutinyError> { + let now = utils::now(); + let labels_map = self.storage.get_invoice_labels()?; + + Ok(list_payment_info(&self.storage, inbound)? + .into_iter() + .filter_map(|(h, i)| { + let labels = match i.bolt11.clone() { + None => vec![], + Some(i) => labels_map.get(&i).cloned().unwrap_or_default(), + }; + let mutiny_invoice = MutinyInvoice::from(i.clone(), h, inbound, labels).ok(); + + // filter out expired invoices + mutiny_invoice.filter(|invoice| { + !invoice.bolt11.as_ref().is_some_and(|b| b.would_expire(now)) + || matches!(invoice.status, HTLCStatus::Succeeded | HTLCStatus::InFlight) + }) + }) + .collect()) + } + /// Gets an invoice. /// This includes sent and received invoices. pub async fn get_invoice(&self, invoice: &Bolt11Invoice) -> Result { diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index bb70d7235..e113187e3 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -1,3 +1,4 @@ +use crate::lsp::{InvoiceRequest, LspConfig}; use crate::nodemanager::ChannelClosure; use crate::peermanager::LspMessageRouter; use crate::storage::MutinyStorage; @@ -12,10 +13,11 @@ use crate::{ ldkstorage::{MutinyNodePersister, PhantomChannelManager}, logging::MutinyLogger, lsp::{AnyLsp, FeeRequest, Lsp}, - nodemanager::{MutinyInvoice, NodeIndex}, + nodemanager::NodeIndex, onchain::OnChainWallet, peermanager::{GossipMessageHandler, PeerManagerImpl}, utils::{self, sleep}, + MutinyInvoice, }; use crate::{fees::P2WSH_OUTPUT_SIZE, peermanager::connect_peer_if_necessary}; use crate::{keymanager::PhantomKeysManager, scorer::HubPreferentialScorer}; @@ -24,10 +26,6 @@ use crate::{ ldkstorage::{persist_monitor, ChannelOpenParams}, storage::persist_payment_info, }; -use crate::{ - lsp::{InvoiceRequest, LspConfig}, - storage::list_payment_info, -}; use crate::{messagehandler::MutinyMessageHandler, storage::read_payment_info}; use anyhow::{anyhow, Context}; use bdk::FeeRate; @@ -1220,38 +1218,6 @@ impl Node { ) } - 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)?; - inbound_invoices.append(&mut outbound_invoices); - Ok(inbound_invoices) - } - - fn list_payment_info_from_persisters( - &self, - inbound: bool, - ) -> Result, MutinyError> { - let now = utils::now(); - let labels_map = self.persister.storage.get_invoice_labels()?; - - Ok(list_payment_info(&self.persister.storage, inbound)? - .into_iter() - .filter_map(|(h, i)| { - let labels = match i.bolt11.clone() { - None => vec![], - Some(i) => labels_map.get(&i).cloned().unwrap_or_default(), - }; - let mutiny_invoice = MutinyInvoice::from(i.clone(), h, inbound, labels).ok(); - - // filter out expired invoices - mutiny_invoice.filter(|invoice| { - !invoice.bolt11.as_ref().is_some_and(|b| b.would_expire(now)) - || matches!(i.status, HTLCStatus::Succeeded | HTLCStatus::InFlight) - }) - }) - .collect()) - } - /// Gets all the closed channels for this node pub fn get_channel_closure( &self, diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index cdcee8a27..e0a15ccb1 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1,8 +1,9 @@ -use crate::event::{HTLCStatus, MillisatAmount, PaymentInfo}; +use crate::event::HTLCStatus; use crate::labels::LabelStorage; use crate::logging::LOGGING_KEY; use crate::utils::{sleep, spawn}; use crate::ActivityItem; +use crate::MutinyInvoice; use crate::MutinyWalletConfig; use crate::{ chain::MutinyChain, @@ -27,7 +28,7 @@ use bdk::chain::{BlockId, ConfirmationTime}; use bdk::{wallet::AddressIndex, FeeRate, LocalUtxo}; use bitcoin::blockdata::script; use bitcoin::hashes::hex::ToHex; -use bitcoin::hashes::{sha256, Hash}; +use bitcoin::hashes::sha256; use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::secp256k1::PublicKey; use bitcoin::util::bip32::ExtendedPrivKey; @@ -39,12 +40,12 @@ use lightning::chain::Confirm; use lightning::events::ClosureReason; use lightning::ln::channelmanager::{ChannelDetails, PhantomRouteHints}; use lightning::ln::script::ShutdownScript; -use lightning::ln::{ChannelId, PaymentHash}; +use lightning::ln::ChannelId; use lightning::routing::gossip::NodeId; use lightning::sign::{NodeSigner, Recipient}; use lightning::util::logger::*; use lightning::{log_debug, log_error, log_info, log_warn}; -use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; +use lightning_invoice::Bolt11Invoice; use lightning_transaction_sync::EsploraSyncClient; use payjoin::Uri; use reqwest::Client; @@ -98,155 +99,6 @@ pub struct MutinyBip21RawMaterials { pub labels: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] -pub struct MutinyInvoice { - pub bolt11: Option, - pub description: Option, - pub payment_hash: sha256::Hash, - pub preimage: Option, - pub payee_pubkey: Option, - pub amount_sats: Option, - pub expire: u64, - pub status: HTLCStatus, - pub fees_paid: Option, - pub inbound: bool, - pub labels: Vec, - pub last_updated: u64, -} - -impl MutinyInvoice { - pub fn paid(&self) -> bool { - self.status == HTLCStatus::Succeeded - } -} - -impl From for MutinyInvoice { - fn from(value: Bolt11Invoice) -> Self { - let description = match value.description() { - Bolt11InvoiceDescription::Direct(a) => { - if a.is_empty() { - None - } else { - Some(a.to_string()) - } - } - Bolt11InvoiceDescription::Hash(_) => None, - }; - - let timestamp = value.duration_since_epoch().as_secs(); - let expiry = timestamp + value.expiry_time().as_secs(); - - let payment_hash = value.payment_hash().to_owned(); - let payee_pubkey = value.payee_pub_key().map(|p| p.to_owned()); - let amount_sats = value.amount_milli_satoshis().map(|m| m / 1000); - - MutinyInvoice { - bolt11: Some(value), - description, - payment_hash, - preimage: None, - payee_pubkey, - amount_sats, - expire: expiry, - status: HTLCStatus::Pending, - fees_paid: None, - inbound: true, - labels: vec![], - last_updated: timestamp, - } - } -} - -impl From for PaymentInfo { - fn from(invoice: MutinyInvoice) -> Self { - let preimage = invoice - .preimage - .map(|s| hex::decode(s).expect("preimage should decode")) - .map(|v| { - let mut arr = [0; 32]; - arr[..].copy_from_slice(&v); - arr - }); - let secret = None; - let status = invoice.status; - let amt_msat = invoice - .amount_sats - .map(|s| MillisatAmount(Some(s))) - .unwrap_or(MillisatAmount(None)); - let fee_paid_msat = invoice.fees_paid; - let bolt11 = invoice.bolt11; - let payee_pubkey = invoice.payee_pubkey; - let last_update = invoice.last_updated; - - PaymentInfo { - preimage, - secret, - status, - amt_msat, - fee_paid_msat, - bolt11, - payee_pubkey, - last_update, - } - } -} - -impl MutinyInvoice { - pub(crate) fn from( - i: PaymentInfo, - payment_hash: PaymentHash, - inbound: bool, - labels: Vec, - ) -> Result { - match i.bolt11 { - Some(invoice) => { - // Construct an invoice from a bolt11, easy - let amount_sats = if let Some(inv_amt) = invoice.amount_milli_satoshis() { - if inv_amt == 0 { - i.amt_msat.0.map(|a| a / 1_000) - } else { - Some(inv_amt / 1_000) - } - } else { - i.amt_msat.0.map(|a| a / 1_000) - }; - Ok(MutinyInvoice { - inbound, - last_updated: i.last_update, - status: i.status, - labels, - amount_sats, - payee_pubkey: i.payee_pubkey, - preimage: i.preimage.map(|p| p.to_hex()), - fees_paid: i.fee_paid_msat.map(|f| f / 1_000), - ..invoice.into() - }) - } - None => { - let amount_sats: Option = i.amt_msat.0.map(|s| s / 1_000); - let fees_paid = i.fee_paid_msat.map(|f| f / 1_000); - let preimage = i.preimage.map(|p| p.to_hex()); - let payment_hash = sha256::Hash::from_inner(payment_hash.0); - let invoice = MutinyInvoice { - bolt11: None, - description: None, - payment_hash, - preimage, - payee_pubkey: i.payee_pubkey, - amount_sats, - expire: i.last_update, - status: i.status, - fees_paid, - inbound, - labels, - last_updated: i.last_update, - }; - Ok(invoice) - } - } - } -} - #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct MutinyPeer { pub pubkey: PublicKey, @@ -1066,17 +918,13 @@ impl NodeManager { } /// Returns all the on-chain and lightning activity from the wallet. - pub(crate) async fn get_activity(&self) -> Result, MutinyError> { + pub(crate) async fn get_activity( + &self, + ) -> Result<(Vec, Vec), MutinyError> { // todo add contacts to the activity - let (lightning, closures) = - futures_util::join!(self.list_invoices(), self.list_channel_closures()); - let lightning = lightning - .map_err(|e| { - log_warn!(self.logger, "Failed to get lightning activity: {e}"); - e - }) - .unwrap_or_default(); - let closures = closures + let closures = self + .list_channel_closures() + .await .map_err(|e| { log_warn!(self.logger, "Failed to get channel closures: {e}"); e @@ -1090,24 +938,7 @@ impl NodeManager { }) .unwrap_or_default(); - let mut activity = Vec::with_capacity(lightning.len() + onchain.len() + closures.len()); - for ln in lightning { - // 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 => {} - } - } - for on in onchain { - activity.push(ActivityItem::OnChain(on)); - } - for chan in closures { - activity.push(ActivityItem::ChannelClosed(chan)); - } - - Ok(activity) + Ok((closures, onchain)) } /// Returns all the on-chain and lightning activity for a given label @@ -1614,19 +1445,6 @@ impl NodeManager { Err(MutinyError::NotFound) } - /// Gets an invoice from the node manager. - /// This includes sent and received invoices. - pub async fn list_invoices(&self) -> Result, MutinyError> { - let mut invoices: Vec = vec![]; - let nodes = self.nodes.lock().await; - for (_, node) in nodes.iter() { - if let Ok(mut invs) = node.list_invoices() { - invoices.append(&mut invs) - } - } - Ok(invoices) - } - pub async fn get_channel_closure( &self, user_channel_id: u128, diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 5dac64a6c..27fba70c6 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -1190,11 +1190,11 @@ mod test { mod wasm_test { use super::*; use crate::logging::MutinyLogger; - use crate::nodemanager::MutinyInvoice; use crate::nostr::ProfileType; use crate::storage::MemoryStorage; use crate::test_utils::{create_dummy_invoice, create_mutiny_wallet, create_nwc_request}; use crate::MockInvoiceHandler; + use crate::MutinyInvoice; use bitcoin::secp256k1::ONE_KEY; use bitcoin::Network; use mockall::predicate::eq; diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index d0711e93b..63bfde369 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -860,9 +860,7 @@ impl MutinyWallet { /// This includes sent and received invoices. #[wasm_bindgen] pub async fn list_invoices(&self) -> Result */, MutinyJsError> { - Ok(JsValue::from_serde( - &self.inner.node_manager.list_invoices().await?, - )?) + Ok(JsValue::from_serde(&self.inner.list_invoices()?)?) } /// Gets an channel closure from the node manager. diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index db055085b..79d76bcde 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -6,6 +6,7 @@ use gloo_utils::format::JsValueSerdeExt; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use lnurl::lightning_address::LightningAddress; use lnurl::lnurl::LnUrl; +use mutiny_core::event::HTLCStatus; use mutiny_core::labels::Contact as MutinyContact; use mutiny_core::nostr::nwc::SpendingConditions; use mutiny_core::*; @@ -172,8 +173,8 @@ impl MutinyInvoice { } } -impl From for MutinyInvoice { - fn from(m: nodemanager::MutinyInvoice) -> Self { +impl From for MutinyInvoice { + fn from(m: mutiny_core::MutinyInvoice) -> Self { let potential_hodl_invoice = match m.bolt11 { Some(ref b) => { utils::HODL_INVOICE_NODES.contains(&b.recover_payee_pub_key().to_hex().as_str())