diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 6be5cba7a..85b7f7d79 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -2,6 +2,7 @@ use crate::esplora::TxSyncError; use aes::cipher::block_padding::UnpadError; use bitcoin::Network; use lightning::ln::peer_handler::PeerHandleError; +use lightning::offers::parse::Bolt12SemanticError; use lightning_invoice::payment::PaymentError; use lightning_invoice::ParseOrSemanticError; use lightning_rapid_gossip_sync::GraphSyncError; @@ -381,3 +382,26 @@ impl From for MutinyError { Self::NostrError } } + +impl From for MutinyError { + fn from(e: Bolt12SemanticError) -> Self { + match e { + Bolt12SemanticError::UnsupportedChain => MutinyError::NetworkMismatch, + Bolt12SemanticError::UnexpectedChain => MutinyError::NetworkMismatch, + Bolt12SemanticError::MissingAmount => MutinyError::BadAmountError, + Bolt12SemanticError::InvalidAmount => MutinyError::BadAmountError, + Bolt12SemanticError::InsufficientAmount => MutinyError::BadAmountError, + Bolt12SemanticError::UnexpectedAmount => MutinyError::BadAmountError, + Bolt12SemanticError::UnsupportedCurrency => MutinyError::BadAmountError, + Bolt12SemanticError::MissingSigningPubkey => MutinyError::PubkeyInvalid, + Bolt12SemanticError::InvalidSigningPubkey => MutinyError::PubkeyInvalid, + Bolt12SemanticError::UnexpectedSigningPubkey => MutinyError::PubkeyInvalid, + Bolt12SemanticError::MissingQuantity => MutinyError::BadAmountError, + Bolt12SemanticError::InvalidQuantity => MutinyError::BadAmountError, + Bolt12SemanticError::UnexpectedQuantity => MutinyError::BadAmountError, + Bolt12SemanticError::MissingPaths => MutinyError::RoutingFailed, + Bolt12SemanticError::AlreadyExpired => MutinyError::PaymentTimeout, + _ => MutinyError::LnDecodeError, + } + } +} diff --git a/mutiny-core/src/event.rs b/mutiny-core/src/event.rs index 1f32adcbf..f906fd612 100644 --- a/mutiny-core/src/event.rs +++ b/mutiny-core/src/event.rs @@ -28,6 +28,8 @@ pub(crate) struct PaymentInfo { #[serde(skip_serializing_if = "Option::is_none")] pub preimage: Option<[u8; 32]>, #[serde(skip_serializing_if = "Option::is_none")] + pub payment_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub secret: Option<[u8; 32]>, pub status: HTLCStatus, #[serde(skip_serializing_if = "MillisatAmount::is_none")] @@ -37,6 +39,8 @@ pub(crate) struct PaymentInfo { #[serde(skip_serializing_if = "Option::is_none")] pub bolt11: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub bolt12: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub payee_pubkey: Option, pub last_update: u64, } @@ -266,6 +270,8 @@ impl EventHandler { let payment_secret = payment_secret.map(|p| p.0); saved_payment_info.status = HTLCStatus::Succeeded; saved_payment_info.preimage = payment_preimage; + // set payment_hash, we won't have it yet for bolt 12 + saved_payment_info.payment_hash = Some(payment_hash.0.to_hex()); saved_payment_info.secret = payment_secret; saved_payment_info.amt_msat = MillisatAmount(Some(amount_msat)); saved_payment_info.last_update = crate::utils::now().as_secs(); @@ -288,12 +294,14 @@ impl EventHandler { let payment_info = PaymentInfo { preimage: payment_preimage, + payment_hash: Some(payment_hash.0.to_hex()), secret: payment_secret, status: HTLCStatus::Succeeded, amt_msat: MillisatAmount(Some(amount_msat)), fee_paid_msat: None, payee_pubkey: receiver_node_id, bolt11: None, + bolt12: None, last_update, }; match self.persister.persist_payment_info( @@ -311,10 +319,10 @@ impl EventHandler { } } Event::PaymentSent { + payment_id, payment_preimage, payment_hash, fee_paid_msat, - .. } => { log_debug!( self.logger, @@ -322,17 +330,28 @@ impl EventHandler { payment_hash.0.to_hex() ); - match self + // lookup by payment hash then payment id, we use payment_id for bolt 12 + let mut lookup_id = payment_hash.0; + let found_payment_info = self .persister - .read_payment_info(&payment_hash.0, false, &self.logger) - { + .read_payment_info(&lookup_id, false, &self.logger) + .or_else(|| { + payment_id.and_then(|id| { + lookup_id = id.0; + self.persister.read_payment_info(&id.0, false, &self.logger) + }) + }); + + match found_payment_info { Some(mut saved_payment_info) => { saved_payment_info.status = HTLCStatus::Succeeded; saved_payment_info.preimage = Some(payment_preimage.0); + // bolt 12 won't have the payment hash set yet + saved_payment_info.payment_hash = Some(payment_hash.0.to_hex()); saved_payment_info.fee_paid_msat = fee_paid_msat; saved_payment_info.last_update = crate::utils::now().as_secs(); match self.persister.persist_payment_info( - &payment_hash.0, + &lookup_id, &saved_payment_info, false, ) { @@ -663,10 +682,12 @@ mod test { let payment_info = PaymentInfo { preimage: Some(preimage), + payment_hash: None, status: HTLCStatus::Succeeded, amt_msat: MillisatAmount(Some(420)), fee_paid_msat: None, bolt11: None, + bolt12: None, payee_pubkey: Some(pubkey), secret: None, last_update: utils::now().as_secs(), diff --git a/mutiny-core/src/labels.rs b/mutiny-core/src/labels.rs index 65c0dbac2..fe1f8deeb 100644 --- a/mutiny-core/src/labels.rs +++ b/mutiny-core/src/labels.rs @@ -2,7 +2,6 @@ use crate::error::MutinyError; use crate::nodemanager::NodeManager; use crate::storage::MutinyStorage; use bitcoin::{Address, XOnlyPublicKey}; -use lightning_invoice::Bolt11Invoice; use lnurl::lightning_address::LightningAddress; use lnurl::lnurl::LnUrl; use nostr::Metadata; @@ -21,7 +20,7 @@ pub struct LabelItem { /// List of addresses that have this label pub addresses: Vec
, /// List of invoices that have this label - pub invoices: Vec, + pub invoices: Vec, /// Epoch time in seconds when this label was last used pub last_used_time: u64, } @@ -89,7 +88,7 @@ pub trait LabelStorage { /// Get a map of addresses to labels. This can be used to get all the labels for an address fn get_address_labels(&self) -> Result>, MutinyError>; /// Get a map of invoices to labels. This can be used to get all the labels for an invoice - fn get_invoice_labels(&self) -> Result>, MutinyError>; + fn get_invoice_labels(&self) -> Result>, MutinyError>; /// Get all the existing labels fn get_labels(&self) -> Result, MutinyError>; /// Get information about a label @@ -101,11 +100,7 @@ pub trait LabelStorage { /// Set the labels for an invoice, replacing any existing labels /// If you do not want to replace any existing labels, use `get_invoice_labels` to get the existing labels, /// add the new labels, and then use `set_invoice_labels` to set the new labels - fn set_invoice_labels( - &self, - invoice: Bolt11Invoice, - labels: Vec, - ) -> Result<(), MutinyError>; + fn set_invoice_labels(&self, invoice: String, labels: Vec) -> Result<(), MutinyError>; /// Get all the existing contacts fn get_contacts(&self) -> Result, MutinyError>; /// Get a contact by label, the label should be a uuid @@ -132,9 +127,8 @@ impl LabelStorage for S { Ok(res.unwrap_or_default()) // if no labels exist, return an empty map } - fn get_invoice_labels(&self) -> Result>, MutinyError> { - let res: Option>> = - self.get_data(INVOICE_LABELS_MAP_KEY)?; + fn get_invoice_labels(&self) -> Result>, MutinyError> { + let res: Option>> = self.get_data(INVOICE_LABELS_MAP_KEY)?; Ok(res.unwrap_or_default()) // if no labels exist, return an empty map } @@ -200,11 +194,7 @@ impl LabelStorage for S { Ok(()) } - fn set_invoice_labels( - &self, - invoice: Bolt11Invoice, - labels: Vec, - ) -> Result<(), MutinyError> { + fn set_invoice_labels(&self, invoice: String, labels: Vec) -> Result<(), MutinyError> { // update the labels map let mut invoice_labels = self.get_invoice_labels()?; invoice_labels.insert(invoice.clone(), labels.clone()); @@ -390,7 +380,7 @@ impl LabelStorage for NodeManager { self.storage.get_address_labels() } - fn get_invoice_labels(&self) -> Result>, MutinyError> { + fn get_invoice_labels(&self) -> Result>, MutinyError> { self.storage.get_invoice_labels() } @@ -406,11 +396,7 @@ impl LabelStorage for NodeManager { self.storage.set_address_labels(address, labels) } - fn set_invoice_labels( - &self, - invoice: Bolt11Invoice, - labels: Vec, - ) -> Result<(), MutinyError> { + fn set_invoice_labels(&self, invoice: String, labels: Vec) -> Result<(), MutinyError> { self.storage.set_invoice_labels(invoice, labels) } @@ -452,7 +438,6 @@ mod tests { use super::*; use crate::test_utils::*; use bitcoin::Address; - use lightning_invoice::Bolt11Invoice; use std::collections::HashMap; use std::str::FromStr; @@ -480,18 +465,18 @@ mod tests { labels } - fn create_test_invoice_labels_map() -> HashMap> { + fn create_test_invoice_labels_map() -> HashMap> { let mut labels = HashMap::new(); labels.insert( - Bolt11Invoice::from_str("lnbc923720n1pj9nrefpp5pczykgk37af5388n8dzynljpkzs7sje4melqgazlwv9y3apay8jqhp5rd8saxz3juve3eejq7z5fjttxmpaq88d7l92xv34n4h3mq6kwq2qcqzzsxqzfvsp5z0jwpehkuz9f2kv96h62p8x30nku76aj8yddpcust7g8ad0tr52q9qyyssqfy622q25helv8cj8hyxqltws4rdwz0xx2hw0uh575mn7a76cp3q4jcptmtjkjs4a34dqqxn8uy70d0qlxqleezv4zp84uk30pp5q3nqq4c9gkz").unwrap(), + String::from("lnbc923720n1pj9nrefpp5pczykgk37af5388n8dzynljpkzs7sje4melqgazlwv9y3apay8jqhp5rd8saxz3juve3eejq7z5fjttxmpaq88d7l92xv34n4h3mq6kwq2qcqzzsxqzfvsp5z0jwpehkuz9f2kv96h62p8x30nku76aj8yddpcust7g8ad0tr52q9qyyssqfy622q25helv8cj8hyxqltws4rdwz0xx2hw0uh575mn7a76cp3q4jcptmtjkjs4a34dqqxn8uy70d0qlxqleezv4zp84uk30pp5q3nqq4c9gkz"), vec!["test1".to_string()], ); labels.insert( - Bolt11Invoice::from_str("lnbc923720n1pj9nre4pp58zjsgd3xkyj33wv6rfmsshg9hqdpqrh8dyaulzwg62x6h3qs39tqhp5vqcr4c3tnxyxr08rk28n8mkphe6c5gfusmyncpmdh604trq3cafqcqzzsxqzfvsp5un4ey9rh0pl23648xtng2k6gtw7w2p6ldaexl6ylwcuhnsnxnsfs9qyyssqxnhr6jvdqfwr97qk7dtsnqaps78r7fjlpyz5z57r2k70az5tvvss4tpucycqpph8gx0vxxr7xse442zf8wxlskln8n77qkd4kad4t5qp92lvrm").unwrap(), + String::from("lnbc923720n1pj9nre4pp58zjsgd3xkyj33wv6rfmsshg9hqdpqrh8dyaulzwg62x6h3qs39tqhp5vqcr4c3tnxyxr08rk28n8mkphe6c5gfusmyncpmdh604trq3cafqcqzzsxqzfvsp5un4ey9rh0pl23648xtng2k6gtw7w2p6ldaexl6ylwcuhnsnxnsfs9qyyssqxnhr6jvdqfwr97qk7dtsnqaps78r7fjlpyz5z57r2k70az5tvvss4tpucycqpph8gx0vxxr7xse442zf8wxlskln8n77qkd4kad4t5qp92lvrm"), vec!["test2".to_string()], ); labels.insert( - Bolt11Invoice::from_str("lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm").unwrap(), + String::from("lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm"), vec!["test3".to_string()], ); labels @@ -510,7 +495,7 @@ mod tests { "test2".to_string(), LabelItem { addresses: vec![Address::from_str("1BitcoinEaterAddressDontSendf59kuE").unwrap()], - invoices: vec![Bolt11Invoice::from_str("lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm").unwrap()], + invoices: vec![String::from("lnbc923720n1pj9nr6zpp5xmvlq2u5253htn52mflh2e6gn7pk5ht0d4qyhc62fadytccxw7hqhp5l4s6qwh57a7cwr7zrcz706qx0qy4eykcpr8m8dwz08hqf362egfscqzzsxqzfvsp5pr7yjvcn4ggrf6fq090zey0yvf8nqvdh2kq7fue0s0gnm69evy6s9qyyssqjyq0fwjr22eeg08xvmz88307yqu8tqqdjpycmermks822fpqyxgshj8hvnl9mkh6srclnxx0uf4ugfq43d66ak3rrz4dqcqd23vxwpsqf7dmhm")], ..Default::default() }, ); @@ -665,7 +650,7 @@ mod tests { let storage = MemoryStorage::default(); - let invoice = Bolt11Invoice::from_str(INVOICE).unwrap(); + let invoice = INVOICE.to_string(); let labels = vec!["label1".to_string(), "label2".to_string()]; let result = storage.set_invoice_labels(invoice.clone(), labels.clone()); @@ -785,7 +770,7 @@ mod tests { let storage = MemoryStorage::default(); let address = Address::from_str(ADDRESS).unwrap(); - let invoice = Bolt11Invoice::from_str(INVOICE).unwrap(); + let invoice = INVOICE.to_string(); let label = "test_label".to_string(); let other_label = "other_label".to_string(); let contact = create_test_contacts().iter().next().unwrap().1.to_owned(); @@ -913,7 +898,7 @@ mod tests { assert_eq!(contact.last_used, 0); let id = storage.create_new_contact(contact.clone()).unwrap(); - let invoice = Bolt11Invoice::from_str(INVOICE).unwrap(); + let invoice = INVOICE.to_string(); storage .set_invoice_labels(invoice, vec![id.clone()]) diff --git a/mutiny-core/src/ldkstorage.rs b/mutiny-core/src/ldkstorage.rs index ff261a0b0..d3f4fb96e 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -792,10 +792,12 @@ mod test { let payment_info = PaymentInfo { preimage: Some(preimage), + payment_hash: Some(payment_hash.0.to_hex()), status: HTLCStatus::Succeeded, amt_msat: MillisatAmount(Some(420)), fee_paid_msat: None, bolt11: None, + bolt12: None, payee_pubkey: Some(pubkey), secret: None, last_update: utils::now().as_secs(), diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index ca531e117..63d4179f2 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -42,6 +42,8 @@ use crate::utils::get_monitor_version; use bitcoin::util::bip32::ExtendedPrivKey; use lightning::events::bump_transaction::{BumpTransactionEventHandler, Wallet}; use lightning::ln::PaymentSecret; +use lightning::offers::offer::Offer; +use lightning::onion_message::{DefaultMessageRouter, OnionMessenger as LdkOnionMessenger}; use lightning::sign::{EntropySource, InMemorySigner, NodeSigner, Recipient}; use lightning::util::config::MaxDustHTLCExposure; use lightning::{ @@ -93,10 +95,19 @@ pub(crate) type RapidGossipSync = pub(crate) type NetworkGraph = gossip::NetworkGraph>; +pub(crate) type OnionMessenger = LdkOnionMessenger< + Arc>, + Arc>, + Arc, + Arc, + Arc>, + IgnoringMessageHandler, +>; + pub(crate) type MessageHandler = LdkMessageHandler< Arc>, Arc>, - Arc, + Arc>, Arc, >; @@ -314,12 +325,21 @@ impl Node { logger: logger.clone(), }); + let onion_message_handler = Arc::new(OnionMessenger::new( + keys_manager.clone(), + keys_manager.clone(), + logger.clone(), + Arc::new(DefaultMessageRouter {}), + channel_manager.clone(), + IgnoringMessageHandler {}, + )); + // init peer manager let scb_message_handler = Arc::new(SCBMessageHandler::new()); let ln_msg_handler = MessageHandler { chan_handler: channel_manager.clone(), route_handler, - onion_message_handler: Arc::new(IgnoringMessageHandler {}), + onion_message_handler, custom_message_handler: scb_message_handler.clone(), }; @@ -896,11 +916,13 @@ impl Node { let payment_hash = PaymentHash(invoice.payment_hash().into_inner()); let payment_info = PaymentInfo { preimage: None, + payment_hash: Some(payment_hash.0.to_hex()), secret: Some(invoice.payment_secret().0), status: HTLCStatus::Pending, amt_msat: MillisatAmount(amount_msat), fee_paid_msat: fee_amount_msat, bolt11: Some(invoice.clone()), + bolt12: None, payee_pubkey: None, last_update, }; @@ -913,7 +935,7 @@ impl Node { self.persister .storage - .set_invoice_labels(invoice.clone(), labels)?; + .set_invoice_labels(invoice.to_string(), labels)?; log_info!(self.logger, "SUCCESS: generated invoice: {invoice}"); @@ -930,12 +952,12 @@ impl Node { let labels = payment_info .bolt11 .as_ref() - .and_then(|inv| labels_map.get(inv).cloned()) + .and_then(|inv| labels_map.get(&inv.to_string()).cloned()) .unwrap_or_default(); MutinyInvoice::from( payment_info, - PaymentHash(payment_hash.into_inner()), + Some(PaymentHash(payment_hash.into_inner())), inbound, labels, ) @@ -960,11 +982,15 @@ impl Node { .list_payment_info(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(), + // lookup bolt 11 then bolt 12 for label + let labels = match i.bolt11 { + None => match i.bolt12 { + None => vec![], + Some(ref bolt12) => labels_map.get(bolt12).cloned().unwrap_or_default(), + }, + Some(ref i) => labels_map.get(&i.to_string()).cloned().unwrap_or_default(), }; - let mutiny_invoice = MutinyInvoice::from(i.clone(), h, inbound, labels).ok(); + let mutiny_invoice = MutinyInvoice::from(i.clone(), Some(h), inbound, labels).ok(); // filter out expired invoices mutiny_invoice.filter(|invoice| { @@ -1101,7 +1127,7 @@ impl Node { if let Err(e) = self .persister .storage - .set_invoice_labels(invoice.clone(), labels) + .set_invoice_labels(invoice.to_string(), labels) { log_error!(self.logger, "could not set invoice label: {e}"); } @@ -1109,11 +1135,13 @@ impl Node { let last_update = utils::now().as_secs(); let mut payment_info = PaymentInfo { preimage: None, + payment_hash: Some(payment_hash.to_hex()), secret: None, status: HTLCStatus::InFlight, amt_msat: MillisatAmount(Some(amt_msat)), fee_paid_msat: None, bolt11: Some(invoice.clone()), + bolt12: None, payee_pubkey: None, last_update, }; @@ -1163,10 +1191,83 @@ impl Node { } } + /// init_offer_payment sends off the payment but does not wait for results + /// use pay_offer_with_timeout to wait for results + pub async fn init_offer_payment( + &self, + offer: Offer, + amt_sats: Option, + quantity: Option, + payer_note: Option, + labels: Vec, + ) -> Result { + if self.channel_manager.list_channels().is_empty() { + // No channels so routing will always fail + return Err(MutinyError::RoutingFailed); + } + + // make sure node at least has one connection before attempting payment + // wait for connection before paying, or otherwise instant fail anyways + for _ in 0..DEFAULT_PAYMENT_TIMEOUT { + // check if we've been stopped + if self.stop.load(Ordering::Relaxed) { + return Err(MutinyError::NotRunning); + } + if !self.channel_manager.list_usable_channels().is_empty() { + break; + } + sleep(1_000).await; + } + + let mut bytes = [0u8; 32]; + getrandom::getrandom(&mut bytes).map_err(|_| MutinyError::SeedGenerationFailed)?; + let payment_id = PaymentId(bytes); + + let amount_msats = amt_sats.map(|a| a * 1_000); + self.channel_manager.pay_for_offer( + &offer, + quantity, + amount_msats, + payer_note, + payment_id, + Self::retry_strategy(), + None, + )?; + + // persist and label offer after calling pay_for_offer, it only fails if we can't initiate a payment + + let offer_string = offer.to_string(); + if let Err(e) = self + .persister + .storage + .set_invoice_labels(offer_string.clone(), labels) + { + log_error!(self.logger, "could not set offer label: {e}"); + } + + let payment_info = PaymentInfo { + preimage: None, + payment_hash: None, + secret: None, + status: HTLCStatus::InFlight, + amt_msat: MillisatAmount(amount_msats), + fee_paid_msat: None, + bolt11: None, + bolt12: Some(offer_string), + payee_pubkey: None, + last_update: utils::now().as_secs(), + }; + + self.persister + .persist_payment_info(&payment_id.0, &payment_info, false)?; + + Ok(payment_id) + } + async fn await_payment( &self, payment_id: PaymentId, - payment_hash: PaymentHash, + payment_hash: Option, timeout: u64, labels: Vec, ) -> Result { @@ -1180,9 +1281,12 @@ impl Node { return Err(MutinyError::PaymentTimeout); } - let payment_info = - self.persister - .read_payment_info(&payment_hash.0, false, &self.logger); + // Use payment hash if we have it, otherwise use payment id + let lookup_id = payment_hash.map(|h| h.0).unwrap_or(payment_id.0); + + let payment_info = self + .persister + .read_payment_info(&lookup_id, false, &self.logger); if let Some(info) = payment_info { match info.status { @@ -1213,10 +1317,28 @@ impl Node { .await?; let timeout: u64 = timeout_secs.unwrap_or(DEFAULT_PAYMENT_TIMEOUT); - self.await_payment(payment_id, payment_hash, timeout, labels) + self.await_payment(payment_id, Some(payment_hash), timeout, labels) .await } + pub async fn pay_offer_with_timeout( + &self, + offer: Offer, + amt_sats: Option, + quantity: Option, + payer_note: Option, + labels: Vec, + timeout_secs: Option, + ) -> Result { + // initiate payment + let payment_id = self + .init_offer_payment(offer, amt_sats, quantity, payer_note, labels.clone()) + .await?; + let timeout: u64 = timeout_secs.unwrap_or(DEFAULT_PAYMENT_TIMEOUT); + + self.await_payment(payment_id, None, timeout, labels).await + } + /// init_keysend_payment sends off the payment but does not wait for results /// use keysend_with_timeout to wait for results pub fn init_keysend_payment( @@ -1270,11 +1392,13 @@ impl Node { let last_update = utils::now().as_secs(); let mut payment_info = PaymentInfo { preimage: Some(preimage.0), + payment_hash: Some(payment_hash.0.to_hex()), secret: None, status: HTLCStatus::InFlight, amt_msat: MillisatAmount(Some(amt_msats)), fee_paid_msat: None, bolt11: None, + bolt12: None, payee_pubkey: Some(to_node), last_update, }; @@ -1285,7 +1409,7 @@ impl Node { match pay_result { Ok(_) => { let mutiny_invoice = - MutinyInvoice::from(payment_info, payment_hash, false, labels)?; + MutinyInvoice::from(payment_info, Some(payment_hash), false, labels)?; Ok(mutiny_invoice) } Err(_) => { @@ -1316,7 +1440,7 @@ impl Node { let timeout: u64 = timeout_secs.unwrap_or(DEFAULT_PAYMENT_TIMEOUT); let payment_hash = PaymentHash(pay.payment_hash.into_inner()); - self.await_payment(payment_id, payment_hash, timeout, labels) + self.await_payment(payment_id, Some(payment_hash), timeout, labels) .await } diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 56f209f96..5c73ae16f 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -32,8 +32,9 @@ use crate::{labels::LabelStorage, subscription::MutinySubscriptionClient}; use anyhow::anyhow; use bdk::chain::{BlockId, ConfirmationTime}; use bdk::{wallet::AddressIndex, LocalUtxo}; +use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script; -use bitcoin::hashes::hex::ToHex; +use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{rand, PublicKey, Secp256k1, SecretKey}; use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; @@ -48,6 +49,7 @@ use lightning::ln::channelmanager::{ChannelDetails, PhantomRouteHints}; use lightning::ln::msgs::DecodeError; use lightning::ln::script::ShutdownScript; use lightning::ln::{ChannelId, PaymentHash}; +use lightning::offers::offer::Offer; use lightning::routing::gossip::NodeId; use lightning::sign::{NodeSigner, Recipient}; use lightning::util::logger::*; @@ -166,6 +168,7 @@ pub struct MutinyBip21RawMaterials { #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct MutinyInvoice { pub bolt11: Option, + pub bolt12: Option, pub description: Option, pub payment_hash: sha256::Hash, pub preimage: Option, @@ -207,6 +210,7 @@ impl From for MutinyInvoice { MutinyInvoice { bolt11: Some(value), + bolt12: None, description, payment_hash, preimage: None, @@ -225,7 +229,7 @@ impl From for MutinyInvoice { impl MutinyInvoice { pub(crate) fn from( i: PaymentInfo, - payment_hash: PaymentHash, + payment_hash: Option, inbound: bool, labels: Vec, ) -> Result { @@ -257,9 +261,17 @@ impl MutinyInvoice { 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 payment_hash = match payment_hash { + Some(hash) => sha256::Hash::from_inner(hash.0), + None => match i.payment_hash { + Some(hex) => sha256::Hash::from_hex(&hex).unwrap(), + None => return Err(MutinyError::InvalidArgumentsError), + }, + }; + let invoice = MutinyInvoice { bolt11: None, + bolt12: i.bolt12, description: None, payment_hash, preimage, @@ -1669,6 +1681,27 @@ impl NodeManager { .await } + /// Pays a lightning offer from the selected node. + /// An amount should only be provided if the offer does not have an amount. + /// The amount should be in satoshis. + pub async fn pay_offer( + &self, + from_node: &PublicKey, + offer: Offer, + amt_sats: Option, + quantity: Option, + payer_note: Option, + labels: Vec, + ) -> Result { + if !offer.supports_chain(ChainHash::using_genesis_block(self.network)) { + return Err(MutinyError::IncorrectNetwork(self.network)); + } + + let node = self.get_node(from_node).await?; + node.pay_offer_with_timeout(offer, amt_sats, quantity, payer_note, labels, None) + .await + } + /// Sends a spontaneous payment to a node from the selected node. /// The amount should be in satoshis. pub async fn keysend( @@ -2870,11 +2903,13 @@ mod tests { let payment_info = PaymentInfo { preimage: Some(preimage), + payment_hash: Some(payment_hash.to_hex()), secret: Some(secret), status: HTLCStatus::Succeeded, amt_msat: MillisatAmount(Some(100_000_000)), fee_paid_msat: None, bolt11: Some(invoice.clone()), + bolt12: None, payee_pubkey: None, last_update: 1681781585, }; @@ -2896,7 +2931,7 @@ mod tests { let actual = MutinyInvoice::from( payment_info, - PaymentHash(payment_hash.into_inner()), + Some(PaymentHash(payment_hash.into_inner())), true, labels, ) @@ -2923,11 +2958,13 @@ mod tests { let payment_info = PaymentInfo { preimage: Some(preimage), + payment_hash: Some(payment_hash.to_hex()), secret: None, status: HTLCStatus::Succeeded, amt_msat: MillisatAmount(Some(100_000)), fee_paid_msat: Some(1_000), bolt11: None, + bolt12: None, payee_pubkey: Some(pubkey), last_update: 1681781585, }; @@ -2949,7 +2986,7 @@ mod tests { let actual = MutinyInvoice::from( payment_info, - PaymentHash(payment_hash.into_inner()), + Some(PaymentHash(payment_hash.into_inner())), false, vec![], ) diff --git a/mutiny-core/src/peermanager.rs b/mutiny-core/src/peermanager.rs index aab2db886..61c2f2180 100644 --- a/mutiny-core/src/peermanager.rs +++ b/mutiny-core/src/peermanager.rs @@ -1,4 +1,4 @@ -use crate::node::NetworkGraph; +use crate::node::{NetworkGraph, OnionMessenger}; use crate::storage::MutinyStorage; use crate::{error::MutinyError, fees::MutinyFeeEstimator}; use crate::{gossip, ldkstorage::PhantomChannelManager, logging::MutinyLogger}; @@ -18,7 +18,7 @@ use lightning::ln::features::{InitFeatures, NodeFeatures}; use lightning::ln::msgs; use lightning::ln::msgs::{LightningError, RoutingMessageHandler}; use lightning::ln::peer_handler::PeerHandleError; -use lightning::ln::peer_handler::{IgnoringMessageHandler, PeerManager as LdkPeerManager}; +use lightning::ln::peer_handler::PeerManager as LdkPeerManager; use lightning::log_warn; use lightning::routing::gossip::NodeId; use lightning::util::logger::Logger; @@ -91,7 +91,7 @@ pub(crate) type PeerManagerImpl = LdkPeerManager< MutinySocketDescriptor, Arc>, Arc>, - Arc, + Arc>, Arc, Arc, Arc>, diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index a7bcfe1af..7856fdcb3 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -1,4 +1,5 @@ use bitcoin::Network; +use lightning::offers::parse::Bolt12ParseError; use lightning_invoice::ParseOrSemanticError; use mutiny_core::error::{MutinyError, MutinyStorageError}; use thiserror::Error; @@ -234,6 +235,12 @@ impl From for MutinyJsError { } } +impl From for MutinyJsError { + fn from(_e: Bolt12ParseError) -> Self { + Self::InvoiceInvalid + } +} + impl From for MutinyJsError { fn from(_e: bitcoin::hashes::hex::Error) -> Self { Self::JsonReadWriteError diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 26c73e57d..4b2abe5e0 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -25,6 +25,7 @@ use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; use futures::lock::Mutex; use gloo_utils::format::JsValueSerdeExt; +use lightning::offers::offer::Offer; use lightning::routing::gossip::NodeId; use lightning_invoice::Bolt11Invoice; use lnurl::lnurl::LnUrl; @@ -669,6 +670,34 @@ impl MutinyWallet { .into()) } + /// Pays a lightning offer from the selected node. + /// An amount should only be provided if the offer does not have an amount. + /// The amount should be in satoshis. + pub async fn pay_offer( + &self, + from_node: String, + offer: String, + amt_sats: Option, + quantity: Option, + payer_note: Option, + labels: &JsValue, /* Vec */ + ) -> Result { + let from_node = PublicKey::from_str(&from_node)?; + let offer = Offer::from_str(&offer)?; + let labels: Vec = labels + .into_serde() + .map_err(|_| MutinyJsError::InvalidArgumentsError)?; + // filter out empty note + let payer_note = payer_note.filter(|p| !p.is_empty()); + + Ok(self + .inner + .node_manager + .pay_offer(&from_node, offer, amt_sats, quantity, payer_note, labels) + .await? + .into()) + } + /// Sends a spontaneous payment to a node from the selected node. /// The amount should be in satoshis. #[wasm_bindgen] @@ -1072,7 +1101,6 @@ impl MutinyWallet { invoice: String, labels: &JsValue, /* Vec */ ) -> Result<(), MutinyJsError> { - let invoice = Bolt11Invoice::from_str(&invoice)?; let labels: Vec = labels .into_serde() .map_err(|_| MutinyJsError::InvalidArgumentsError)?;