diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 88d5485d1..c3ae1de28 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; @@ -446,3 +447,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 115a21b09..774c0922a 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, ) { @@ -666,10 +685,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 d6249fa7d..40a305eca 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, } @@ -96,7 +95,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 @@ -108,11 +107,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 @@ -139,9 +134,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 } @@ -207,11 +201,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()); @@ -397,7 +387,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() } @@ -413,11 +403,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) } @@ -459,7 +445,6 @@ mod tests { use super::*; use crate::test_utils::*; use bitcoin::Address; - use lightning_invoice::Bolt11Invoice; use std::collections::HashMap; use std::str::FromStr; @@ -487,18 +472,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 @@ -517,7 +502,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() }, ); @@ -672,7 +657,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()); @@ -792,7 +777,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(); @@ -920,7 +905,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..33f251033 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -5,9 +5,10 @@ use crate::gossip::PROB_SCORER_KEY; use crate::keymanager::PhantomKeysManager; use crate::logging::MutinyLogger; use crate::multiesplora::MultiEsploraClient; +use crate::node::NetworkGraph; use crate::node::{default_user_config, ChainMonitor}; -use crate::node::{NetworkGraph, Router}; use crate::nodemanager::ChannelClosure; +use crate::router::MutinyRouter; use crate::storage::{MutinyStorage, VersionedValue}; use crate::utils; use crate::utils::{sleep, spawn}; @@ -31,7 +32,7 @@ use lightning::sign::{InMemorySigner, SpendableOutputDescriptor, WriteableEcdsaC use lightning::util::logger::Logger; use lightning::util::persist::Persister; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; -use lightning::{log_debug, log_error, log_trace}; +use lightning::{log_debug, log_error}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::io; @@ -53,7 +54,7 @@ pub(crate) type PhantomChannelManager = LdkChannelManager< Arc>, Arc>, Arc>, - Arc, + Arc, Arc, >; @@ -202,7 +203,7 @@ impl MutinyNodePersister { fee_estimator: Arc>, mutiny_logger: Arc, keys_manager: Arc>, - router: Arc, + router: Arc, channel_monitors: Vec<(BlockHash, ChannelMonitor)>, esplora: &MultiEsploraClient, ) -> Result, MutinyError> { @@ -270,7 +271,7 @@ impl MutinyNodePersister { fee_estimator: Arc>, mutiny_logger: Arc, keys_manager: Arc>, - router: Arc, + router: Arc, mut channel_monitors: Vec<(BlockHash, ChannelMonitor)>, ) -> Result, MutinyError> { let mut channel_monitor_mut_references = Vec::new(); @@ -312,7 +313,7 @@ impl MutinyNodePersister { fee_estimator: Arc>, mutiny_logger: Arc, keys_manager: Arc>, - router: Arc, + router: Arc, channel_monitors: Vec<(BlockHash, ChannelMonitor)>, esplora: &MultiEsploraClient, ) -> Result, MutinyError> { @@ -372,10 +373,10 @@ impl MutinyNodePersister { &self, payment_hash: &[u8; 32], inbound: bool, - logger: &MutinyLogger, + _logger: &MutinyLogger, ) -> Option { let key = self.get_key(payment_key(inbound, payment_hash).as_str()); - log_trace!(logger, "Trace: checking payment key: {key}"); + // log_trace!(logger, "Trace: checking payment key: {key}"); let deserialized_value: Result, MutinyError> = self.storage.get_data(key); deserialized_value.ok().flatten() @@ -619,7 +620,7 @@ impl Arc>, Arc>, Arc>, - Arc, + Arc, Arc, utils::Mutex, > for MutinyNodePersister @@ -744,6 +745,7 @@ pub(crate) async fn persist_monitor( #[cfg(test)] mod test { use crate::onchain::OnChainWallet; + use crate::router::MutinyRouter; use crate::storage::MemoryStorage; use crate::{esplora::EsploraSyncClient, node::scoring_params}; use crate::{ @@ -757,7 +759,6 @@ mod test { use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Txid; use esplora_client::Builder; - use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::ProbabilisticScoringDecayParameters; use lightning::sign::EntropySource; use std::str::FromStr; @@ -792,10 +793,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(), @@ -965,8 +968,9 @@ mod test { persister.clone(), )); - let router: Arc = Arc::new(DefaultRouter::new( + let router: Arc = Arc::new(MutinyRouter::new( network_graph, + None, logger.clone(), km.clone().get_secure_random_bytes(), Arc::new(utils::Mutex::new(scorer)), diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index e83d763c5..1a004e6e3 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -34,6 +34,7 @@ pub mod nostr; mod onchain; mod peermanager; pub mod redshift; +mod router; pub mod scorer; pub mod storage; mod subscription; diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index e2bfeb2c6..df0cb22ff 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -3,6 +3,7 @@ use crate::ldkstorage::{persist_monitor, ChannelOpenParams}; use crate::multiesplora::MultiEsploraClient; use crate::nodemanager::ChannelClosure; use crate::peermanager::LspMessageRouter; +use crate::router::MutinyRouter; use crate::utils::get_monitor_version; use crate::{ background::process_events_async, @@ -34,6 +35,7 @@ use futures_util::lock::Mutex; use lightning::events::bump_transaction::{BumpTransactionEventHandler, Wallet}; use lightning::ln::channelmanager::ChannelDetails; use lightning::ln::PaymentSecret; +use lightning::offers::offer::{Offer, Quantity}; use lightning::onion_message::OnionMessenger as LdkOnionMessenger; use lightning::routing::scoring::ProbabilisticScoringDecayParameters; use lightning::sign::{EntropySource, InMemorySigner, NodeSigner, Recipient}; @@ -50,7 +52,7 @@ use lightning::{ routing::{ gossip, gossip::NodeId, - router::{DefaultRouter, PaymentParameters, RouteParameters}, + router::{PaymentParameters, RouteParameters}, }, util::{ config::{ChannelHandshakeConfig, ChannelHandshakeLimits, UserConfig}, @@ -117,14 +119,6 @@ pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< Arc>, >; -pub(crate) type Router = DefaultRouter< - Arc, - Arc, - Arc>, - ProbabilisticScoringFeeParameters, - HubPreferentialScorer, ->; - #[derive(Clone, Debug, Eq, PartialEq)] pub enum ConnectionType { Tcp(String), @@ -247,8 +241,27 @@ impl Node { let network_graph = gossip_sync.network_graph().clone(); - let router: Arc = Arc::new(DefaultRouter::new( + log_info!(logger, "creating lsp client"); + let lsp_client: Option = match node_index.lsp { + None => { + if lsp_clients.is_empty() { + log_info!(logger, "no lsp saved and no lsp clients available"); + None + } else { + log_info!(logger, "no lsp saved, picking random one"); + // If we don't have an lsp saved we should pick a random + // one from our client list and save it for next time + let rand = rand::random::() % lsp_clients.len(); + Some(lsp_clients[rand].clone()) + } + } + Some(ref lsp) => lsp_clients.iter().find(|c| &c.url == lsp).cloned(), + }; + let lsp_pubkey = lsp_client.as_ref().map(|l| l.pubkey); + + let router: Arc = Arc::new(MutinyRouter::new( network_graph, + lsp_pubkey, logger.clone(), keys_manager.clone().get_secure_random_bytes(), scorer.clone(), @@ -926,11 +939,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, }; @@ -943,13 +958,50 @@ 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}"); Ok(invoice) } + pub async fn create_offer( + &self, + amount_sat: Option, + labels: Vec, + ) -> Result { + // wait for first sync to complete + for _ in 0..60 { + // check if we've been stopped + if self.stop.load(Ordering::Relaxed) { + return Err(MutinyError::NotRunning); + } + + if let Ok(true) = self.persister.storage.has_done_first_sync() { + break; + } + + sleep(1_000).await; + } + + let mut builder = self + .channel_manager + .create_offer_builder("".to_string()) + .supported_quantity(Quantity::Unbounded); + + if let Some(amount_sats) = amount_sat { + builder = builder.amount_msats(amount_sats * 1_000); + } + + let offer = builder.build()?; + + self.persister + .storage + .set_invoice_labels(offer.to_string(), labels)?; + + Ok(offer) + } + pub fn get_invoice(&self, invoice: &Bolt11Invoice) -> Result { self.get_invoice_by_hash(invoice.payment_hash()) } @@ -960,12 +1012,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, ) @@ -990,11 +1042,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| { @@ -1138,7 +1194,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}"); } @@ -1146,11 +1202,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, }; @@ -1219,10 +1277,99 @@ 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 quantity = match quantity { + None => { + if offer.expects_quantity() { + Some(1) + } else { + None + } + } + Some(q) => Some(q), + }; + + 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, + ) + .map_err(|e| { + log_error!(self.logger, "failed to make payment: {e:?}"); + e + })?; + + // 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 { @@ -1236,9 +1383,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 { @@ -1269,10 +1419,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( @@ -1325,11 +1493,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, }; @@ -1340,7 +1510,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(error) => { @@ -1376,7 +1546,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 } @@ -2126,13 +2296,16 @@ mod tests { let invoice_labels = storage.get_invoice_labels().unwrap(); assert_eq!(invoice_labels.len(), 1); - assert_eq!(invoice_labels.get(&invoice).cloned(), Some(labels)); + assert_eq!( + invoice_labels.get(&invoice.to_string()).cloned(), + Some(labels) + ); let label_item = storage.get_label("test").unwrap().unwrap(); 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, vec![invoice.to_string()]); } #[tokio::test] @@ -2166,18 +2339,20 @@ mod tests { // check that we get PaymentTimeout if we don't have the payment info let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert_eq!(result.unwrap_err(), MutinyError::PaymentTimeout); let mut payment_info = PaymentInfo { preimage: None, + payment_hash: None, secret: Some([0; 32]), status: HTLCStatus::InFlight, amt_msat: MillisatAmount(Some(1000)), fee_paid_msat: None, bolt11: None, + bolt12: None, payee_pubkey: None, last_update: crate::utils::now().as_secs(), }; @@ -2189,7 +2364,7 @@ mod tests { .unwrap(); let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert_eq!(result.unwrap_err(), MutinyError::PaymentTimeout); @@ -2202,7 +2377,7 @@ mod tests { .unwrap(); let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert_eq!(result.unwrap_err(), MutinyError::RoutingFailed); @@ -2215,7 +2390,7 @@ mod tests { .unwrap(); let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert!(result.is_ok()); @@ -2290,13 +2465,16 @@ mod wasm_test { let invoice_labels = storage.get_invoice_labels().unwrap(); assert_eq!(invoice_labels.len(), 1); - assert_eq!(invoice_labels.get(&invoice).cloned(), Some(labels)); + assert_eq!( + invoice_labels.get(&invoice.to_string()).cloned(), + Some(labels) + ); let label_item = storage.get_label("test").unwrap().unwrap(); 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, vec![invoice.to_string()]); } #[test] @@ -2330,18 +2508,20 @@ mod wasm_test { // check that we get PaymentTimeout if we don't have the payment info let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert_eq!(result.unwrap_err(), MutinyError::PaymentTimeout); let mut payment_info = PaymentInfo { preimage: None, + payment_hash: None, secret: Some([0; 32]), status: HTLCStatus::InFlight, amt_msat: MillisatAmount(Some(1000)), fee_paid_msat: None, bolt11: None, + bolt12: None, payee_pubkey: None, last_update: crate::utils::now().as_secs(), }; @@ -2353,7 +2533,7 @@ mod wasm_test { .unwrap(); let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert_eq!(result.unwrap_err(), MutinyError::PaymentTimeout); @@ -2366,7 +2546,7 @@ mod wasm_test { .unwrap(); let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert_eq!(result.unwrap_err(), MutinyError::RoutingFailed); @@ -2379,7 +2559,7 @@ mod wasm_test { .unwrap(); let result = node - .await_payment(payment_id, payment_hash, 1, vec![]) + .await_payment(payment_id, Some(payment_hash), 1, vec![]) .await; assert!(result.is_ok()); diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 118152565..b0f6c704a 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -28,8 +28,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}; use bitcoin::util::bip32::ExtendedPrivKey; @@ -42,6 +43,7 @@ use lightning::events::ClosureReason; use lightning::ln::channelmanager::{ChannelDetails, PhantomRouteHints}; 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::*; @@ -102,6 +104,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, @@ -143,6 +146,7 @@ impl From for MutinyInvoice { MutinyInvoice { bolt11: Some(value), + bolt12: None, description, payment_hash, preimage: None, @@ -161,7 +165,7 @@ impl From for MutinyInvoice { impl MutinyInvoice { pub(crate) fn from( i: PaymentInfo, - payment_hash: PaymentHash, + payment_hash: Option, inbound: bool, labels: Vec, ) -> Result { @@ -193,9 +197,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, @@ -251,6 +263,7 @@ pub struct MutinyChannel { pub confirmations_required: Option, pub confirmations: u32, pub is_outbound: bool, + pub inbound_scid_alias: Option, } impl From<&ChannelDetails> for MutinyChannel { @@ -265,6 +278,7 @@ impl From<&ChannelDetails> for MutinyChannel { confirmations_required: c.confirmations_required, confirmations: c.confirmations.unwrap_or(0), is_outbound: c.is_outbound, + inbound_scid_alias: c.inbound_scid_alias, } } } @@ -1244,13 +1258,18 @@ impl NodeManager { let mut activity = vec![]; for inv in label_item.invoices.iter() { - let ln = self.get_invoice(inv).await?; - // Only show paid and in-flight invoices - match ln.status { - HTLCStatus::Succeeded | HTLCStatus::InFlight => { - activity.push(ActivityItem::Lightning(Box::new(ln))); + match Bolt11Invoice::from_str(inv) { + Ok(invoice) => { + let ln = self.get_invoice(&invoice).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 => {} + } } - HTLCStatus::Pending | HTLCStatus::Failed => {} + Err(_) => todo!("handle non bolt12 invoices"), } } let onchain = self @@ -1678,6 +1697,20 @@ impl NodeManager { Ok(invoice.into()) } + /// Creates a lightning offer. The amount should be in satoshis. + /// If no amount is provided, the offer will be created with no amount. + pub async fn create_offer( + &self, + from_node: &PublicKey, + amount: Option, + labels: Vec, + ) -> Result { + let node = self.get_node(from_node).await?; + let offer = node.create_offer(amount, labels).await?; + + Ok(offer) + } + /// Pays a lightning invoice from the selected node. /// An amount should only be provided if the invoice does not have an amount. /// The amount should be in satoshis. @@ -1697,6 +1730,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( @@ -2774,17 +2828,20 @@ 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, }; let expected: MutinyInvoice = MutinyInvoice { bolt11: Some(invoice), + bolt12: None, description: None, payment_hash, preimage: Some(preimage.to_hex()), @@ -2800,7 +2857,7 @@ mod tests { let actual = MutinyInvoice::from( payment_info, - PaymentHash(payment_hash.into_inner()), + Some(PaymentHash(payment_hash.into_inner())), true, labels, ) @@ -2827,17 +2884,20 @@ 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, }; let expected: MutinyInvoice = MutinyInvoice { bolt11: None, + bolt12: None, description: None, payment_hash, preimage: Some(preimage.to_hex()), @@ -2853,7 +2913,7 @@ mod tests { let actual = MutinyInvoice::from( payment_info, - PaymentHash(payment_hash.into_inner()), + Some(PaymentHash(payment_hash.into_inner())), false, vec![], ) @@ -2911,6 +2971,7 @@ mod tests { let invoice1: MutinyInvoice = MutinyInvoice { bolt11: None, + bolt12: None, description: None, payment_hash, preimage: Some(preimage.to_hex()), @@ -2926,6 +2987,7 @@ mod tests { let invoice2: MutinyInvoice = MutinyInvoice { bolt11: None, + bolt12: None, description: None, payment_hash, preimage: Some(preimage.to_hex()), @@ -2941,6 +3003,7 @@ mod tests { let invoice3: MutinyInvoice = MutinyInvoice { bolt11: None, + bolt12: None, description: None, payment_hash, preimage: None, @@ -2956,6 +3019,7 @@ mod tests { let invoice4: MutinyInvoice = MutinyInvoice { bolt11: None, + bolt12: None, description: None, payment_hash, preimage: None, @@ -2971,6 +3035,7 @@ mod tests { let invoice5: MutinyInvoice = MutinyInvoice { bolt11: None, + bolt12: None, description: Some("difference".to_string()), payment_hash, preimage: Some(preimage.to_hex()), diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 529861108..cfab43440 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -1225,11 +1225,13 @@ mod wasm_test { let invoice = create_dummy_invoice(Some(1_000), Network::Regtest, None); let payment_info = PaymentInfo { preimage: None, + payment_hash: None, secret: Some(invoice.payment_secret().0), status: HTLCStatus::InFlight, amt_msat: MillisatAmount(invoice.amount_milli_satoshis()), fee_paid_msat: None, bolt11: Some(invoice.clone()), + bolt12: None, payee_pubkey: None, last_update: utils::now().as_secs(), }; @@ -1245,11 +1247,13 @@ mod wasm_test { let invoice = create_dummy_invoice(Some(1_000), Network::Regtest, None); let payment_info = PaymentInfo { preimage: None, + payment_hash: None, secret: Some(invoice.payment_secret().0), status: HTLCStatus::Succeeded, amt_msat: MillisatAmount(invoice.amount_milli_satoshis()), fee_paid_msat: None, bolt11: Some(invoice.clone()), + bolt12: None, payee_pubkey: None, last_update: utils::now().as_secs(), }; diff --git a/mutiny-core/src/router.rs b/mutiny-core/src/router.rs new file mode 100644 index 000000000..f2077ff2f --- /dev/null +++ b/mutiny-core/src/router.rs @@ -0,0 +1,156 @@ +use crate::logging::MutinyLogger; +use crate::node::NetworkGraph; +use crate::scorer::HubPreferentialScorer; +use crate::utils::Mutex; +use bitcoin::secp256k1::PublicKey; +use lightning::ln::channelmanager::ChannelDetails; +use lightning::ln::features::ChannelFeatures; +use lightning::ln::msgs::LightningError; +use lightning::routing::gossip::NodeId; +use lightning::routing::router::{ + BlindedTail, DefaultRouter, InFlightHtlcs, Path, Payee, Route, RouteHop, RouteParameters, + Router, +}; +use lightning::routing::scoring::ProbabilisticScoringFeeParameters; +use lightning::util::ser::Writeable; +use log::warn; +use std::sync::Arc; + +type LdkRouter = DefaultRouter< + Arc, + Arc, + Arc>, + ProbabilisticScoringFeeParameters, + HubPreferentialScorer, +>; + +pub struct MutinyRouter { + network_graph: Arc, + lsp_key: Option, + router: LdkRouter, +} + +impl MutinyRouter { + pub fn new( + network_graph: Arc, + lsp_key: Option, + logger: Arc, + random_seed_bytes: [u8; 32], + scorer: Arc>, + score_params: ProbabilisticScoringFeeParameters, + ) -> Self { + let router = DefaultRouter::new( + network_graph.clone(), + logger, + random_seed_bytes, + scorer, + score_params, + ); + + Self { + network_graph, + lsp_key, + router, + } + } +} + +impl Router for MutinyRouter { + fn find_route( + &self, + payer: &PublicKey, + route_params: &RouteParameters, + first_hops: Option<&[&ChannelDetails]>, + inflight_htlcs: InFlightHtlcs, + ) -> Result { + match &route_params.payment_params.payee { + Payee::Clear { .. } => { + self.router + .find_route(payer, route_params, first_hops, inflight_htlcs) + } + Payee::Blinded { + route_hints, + features: _, + } => { + // if we have no LSP, then handle normally + if self.lsp_key.is_none() { + return self + .router + .find_route(payer, route_params, first_hops, inflight_htlcs); + } + + let (blinded_info, blinded_path) = route_hints.first().unwrap(); + let graph_lock = self.network_graph.read_only(); + let lsp_node_id = NodeId::from_pubkey(&self.lsp_key.unwrap()); + let node_info = graph_lock.node(&lsp_node_id).unwrap(); + + let amt = route_params.final_value_msat; + + // first our channel with enough capacity + let first_hops = first_hops.unwrap_or(&[]); + let first = first_hops + .iter() + .find(|c| c.outbound_capacity_msat >= amt) + .unwrap(); + + let channel_features = + ChannelFeatures::from_be_bytes(first.counterparty.features.encode()); + + let scid = scid_from_parts(467591, 1, 0); + warn!("scid: {}", scid); + + let cltv_expiry_delta = first.config.unwrap().cltv_expiry_delta; + let hops = vec![ + RouteHop { + pubkey: self.lsp_key.unwrap(), + node_features: node_info + .announcement_info + .as_ref() + .unwrap() + .features + .clone(), + short_channel_id: first.get_outbound_payment_scid().unwrap(), + channel_features: channel_features.clone(), + fee_msat: 0, // 0 for own channel + cltv_expiry_delta: 0, // 0 for own channel + maybe_announced_channel: false, + }, + RouteHop { + pubkey: blinded_path.introduction_node_id, + node_features: node_info + .announcement_info + .as_ref() + .unwrap() + .features + .clone(), + short_channel_id: 17112782831943311000, // fixme + channel_features, + fee_msat: 10_000, // put high value just to try + cltv_expiry_delta: cltv_expiry_delta as u32, + maybe_announced_channel: false, + }, + ]; + + let blinded_tail = Some(BlindedTail { + hops: blinded_path.blinded_hops.clone(), + blinding_point: blinded_path.blinding_point, + excess_final_cltv_expiry_delta: blinded_info.cltv_expiry_delta as u32, + final_value_msat: amt, + }); + + let path = Path { hops, blinded_tail }; + + Ok(Route { + paths: vec![path], + route_params: Some(route_params.clone()), + }) + } + } + } +} + +/// Constructs a `short_channel_id` using the components pieces. Results in an error +/// if the block height, tx index, or vout index overflow the maximum sizes. +pub fn scid_from_parts(block: u64, tx_index: u64, vout_index: u64) -> u64 { + (block << 40) | (tx_index << 16) | vout_index +} diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index 42bcef43e..63881a14e 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; @@ -240,6 +241,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 66e3a88f4..15aba67af 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -24,6 +24,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; @@ -684,6 +685,27 @@ impl MutinyWallet { .into()) } + /// Creates a lightning offer. The amount should be in satoshis. + /// If no amount is provided, the offer will be created with no amount. + #[wasm_bindgen] + pub async fn create_offer( + &self, + from_node: String, + amount: Option, + labels: &JsValue, /* Vec */ + ) -> Result { + let from_node = PublicKey::from_str(&from_node)?; + let labels: Vec = labels + .into_serde() + .map_err(|_| MutinyJsError::InvalidArgumentsError)?; + Ok(self + .inner + .node_manager + .create_offer(&from_node, amount, labels) + .await? + .to_string()) + } + /// Pays a lightning invoice from the selected node. /// An amount should only be provided if the invoice does not have an amount. /// The amount should be in satoshis. @@ -705,6 +727,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] @@ -1101,7 +1151,6 @@ impl MutinyWallet { invoice: String, labels: Vec, ) -> Result<(), MutinyJsError> { - let invoice = Bolt11Invoice::from_str(&invoice)?; Ok(self .inner .node_manager diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index 8001ebb7f..2cf5eba66 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -270,6 +270,7 @@ pub struct MutinyChannel { pub confirmations_required: Option, pub confirmations: u32, pub is_outbound: bool, + pub inbound_scid_alias: Option, } #[wasm_bindgen] @@ -309,6 +310,7 @@ impl From for MutinyChannel { confirmations_required: m.confirmations_required, confirmations: m.confirmations, is_outbound: m.is_outbound, + inbound_scid_alias: m.inbound_scid_alias, } } }