diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 58fab0d52..328dcfdbd 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -54,6 +54,7 @@ interface Node { PublicKey node_id(); sequence? listening_addresses(); Bolt11Payment bolt11_payment(); + Bolt12Payment bolt12_payment(); SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); [Throws=NodeError] @@ -99,6 +100,21 @@ interface Bolt11Payment { Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); }; +interface Bolt12Payment { + [Throws=NodeError] + PaymentId send([ByRef]Offer offer, string? payer_note); + [Throws=NodeError] + PaymentId send_using_amount([ByRef]Offer offer, string? payer_note, u64 amount_msat); + [Throws=NodeError] + Offer receive(u64 amount_msat, [ByRef]string description); + [Throws=NodeError] + Offer receive_variable_amount([ByRef]string description); + [Throws=NodeError] + Bolt12Invoice request_refund_payment([ByRef]Refund refund); + [Throws=NodeError] + Refund initiate_refund(u64 amount_msat, u32 expiry_secs); +}; + interface SpontaneousPayment { [Throws=NodeError] PaymentId send(u64 amount_msat, PublicKey node_id); @@ -122,6 +138,9 @@ enum NodeError { "OnchainTxCreationFailed", "ConnectionFailed", "InvoiceCreationFailed", + "InvoiceRequestCreationFailed", + "OfferCreationFailed", + "RefundCreationFailed", "PaymentSendingFailed", "ProbeSendingFailed", "ChannelCreationFailed", @@ -139,15 +158,19 @@ enum NodeError { "InvalidSocketAddress", "InvalidPublicKey", "InvalidSecretKey", + "InvalidOfferId", "InvalidPaymentId", "InvalidPaymentHash", "InvalidPaymentPreimage", "InvalidPaymentSecret", "InvalidAmount", "InvalidInvoice", + "InvalidOffer", + "InvalidRefund", "InvalidChannelId", "InvalidNetwork", "DuplicatePayment", + "UnsupportedCurrency", "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", @@ -225,6 +248,8 @@ interface PaymentKind { Onchain(); Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret); Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, LSPFeeLimits lsp_fee_limits); + Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id); + Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); }; @@ -371,6 +396,18 @@ typedef string Address; [Custom] typedef string Bolt11Invoice; +[Custom] +typedef string Offer; + +[Custom] +typedef string Refund; + +[Custom] +typedef string Bolt12Invoice; + +[Custom] +typedef string OfferId; + [Custom] typedef string PaymentId; diff --git a/src/builder.rs b/src/builder.rs index 6d3db420f..386deb418 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -785,7 +785,7 @@ fn build_with_store_internal( Arc::clone(&logger), Arc::clone(&channel_manager), Arc::new(message_router), - IgnoringMessageHandler {}, + Arc::clone(&channel_manager), IgnoringMessageHandler {}, )); let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); diff --git a/src/error.rs b/src/error.rs index 5acc75af8..824bde192 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,12 @@ pub enum Error { ConnectionFailed, /// Invoice creation failed. InvoiceCreationFailed, + /// Invoice request creation failed. + InvoiceRequestCreationFailed, + /// Offer creation failed. + OfferCreationFailed, + /// Refund creation failed. + RefundCreationFailed, /// Sending a payment has failed. PaymentSendingFailed, /// Sending a payment probe has failed. @@ -47,6 +53,8 @@ pub enum Error { InvalidPublicKey, /// The given secret key is invalid. InvalidSecretKey, + /// The given offer id is invalid. + InvalidOfferId, /// The given payment id is invalid. InvalidPaymentId, /// The given payment hash is invalid. @@ -59,12 +67,18 @@ pub enum Error { InvalidAmount, /// The given invoice is invalid. InvalidInvoice, + /// The given offer is invalid. + InvalidOffer, + /// The given refund is invalid. + InvalidRefund, /// The given channel ID is invalid. InvalidChannelId, /// The given network is invalid. InvalidNetwork, /// A payment with the given hash has already been initiated. DuplicatePayment, + /// The provided offer was denonminated in an unsupported currency. + UnsupportedCurrency, /// The available funds are insufficient to complete the given operation. InsufficientFunds, /// The given operation failed due to the required liquidity source being unavailable. @@ -83,6 +97,9 @@ impl fmt::Display for Error { }, Self::ConnectionFailed => write!(f, "Network connection closed."), Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."), + Self::InvoiceRequestCreationFailed => write!(f, "Failed to create invoice request."), + Self::OfferCreationFailed => write!(f, "Failed to create offer."), + Self::RefundCreationFailed => write!(f, "Failed to create refund."), Self::PaymentSendingFailed => write!(f, "Failed to send the given payment."), Self::ProbeSendingFailed => write!(f, "Failed to send the given payment probe."), Self::ChannelCreationFailed => write!(f, "Failed to create channel."), @@ -102,12 +119,15 @@ impl fmt::Display for Error { Self::InvalidSocketAddress => write!(f, "The given network address is invalid."), Self::InvalidPublicKey => write!(f, "The given public key is invalid."), Self::InvalidSecretKey => write!(f, "The given secret key is invalid."), + Self::InvalidOfferId => write!(f, "The given offer id is invalid."), Self::InvalidPaymentId => write!(f, "The given payment id is invalid."), Self::InvalidPaymentHash => write!(f, "The given payment hash is invalid."), Self::InvalidPaymentPreimage => write!(f, "The given payment preimage is invalid."), Self::InvalidPaymentSecret => write!(f, "The given payment secret is invalid."), Self::InvalidAmount => write!(f, "The given amount is invalid."), Self::InvalidInvoice => write!(f, "The given invoice is invalid."), + Self::InvalidOffer => write!(f, "The given offer is invalid."), + Self::InvalidRefund => write!(f, "The given refund is invalid."), Self::InvalidChannelId => write!(f, "The given channel ID is invalid."), Self::InvalidNetwork => write!(f, "The given network is invalid."), Self::DuplicatePayment => { @@ -116,6 +136,9 @@ impl fmt::Display for Error { Self::InsufficientFunds => { write!(f, "The available funds are insufficient to complete the given operation.") }, + Self::UnsupportedCurrency => { + write!(f, "The provided offer was denonminated in an unsupported currency.") + }, Self::LiquiditySourceUnavailable => { write!(f, "The given operation failed due to the required liquidity source being unavailable.") }, diff --git a/src/event.rs b/src/event.rs index 78188452f..b49fc96e8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,8 +1,11 @@ use crate::types::{DynStore, Sweeper, Wallet}; + use crate::{ hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, }; +use crate::connection::ConnectionManager; + use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, @@ -315,6 +318,7 @@ where event_queue: Arc>, wallet: Arc, channel_manager: Arc, + connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, payment_store: Arc>, @@ -330,14 +334,16 @@ where { pub fn new( event_queue: Arc>, wallet: Arc, channel_manager: Arc, - output_sweeper: Arc, network_graph: Arc, - payment_store: Arc>, peer_store: Arc>, - runtime: Arc>>, logger: L, config: Arc, + connection_manager: Arc>, output_sweeper: Arc, + network_graph: Arc, payment_store: Arc>, + peer_store: Arc>, runtime: Arc>>, + logger: L, config: Arc, ) -> Self { Self { event_queue, wallet, channel_manager, + connection_manager, output_sweeper, network_graph, payment_store, @@ -429,7 +435,9 @@ where } => { let payment_id = PaymentId(payment_hash.0); if let Some(info) = self.payment_store.get(&payment_id) { - if info.status == PaymentStatus::Succeeded { + if info.status == PaymentStatus::Succeeded + || matches!(info.kind, PaymentKind::Spontaneous { .. }) + { log_info!( self.logger, "Refused duplicate inbound payment from payment hash {} of {}msat", @@ -477,6 +485,7 @@ where self.channel_manager.fail_htlc_backwards(&payment_hash); let update = PaymentDetailsUpdate { + hash: Some(Some(payment_hash)), status: Some(PaymentStatus::Failed), ..PaymentDetailsUpdate::new(payment_id) }; @@ -495,126 +504,56 @@ where amount_msat, ); let payment_preimage = match purpose { - PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret } => { - if payment_preimage.is_some() { - payment_preimage - } else { - self.channel_manager - .get_payment_preimage(payment_hash, payment_secret) - .ok() - } - }, - PaymentPurpose::Bolt12OfferPayment { .. } => { - // TODO: support BOLT12. - log_error!( - self.logger, - "Failed to claim unsupported BOLT12 payment with hash: {}", - payment_hash - ); - self.channel_manager.fail_htlc_backwards(&payment_hash); - return; - }, - PaymentPurpose::Bolt12RefundPayment { .. } => { - // TODO: support BOLT12. - log_error!( - self.logger, - "Failed to claim unsupported BOLT12 payment with hash: {}", - payment_hash - ); - self.channel_manager.fail_htlc_backwards(&payment_hash); - return; + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => { + payment_preimage }, - PaymentPurpose::SpontaneousPayment(preimage) => Some(preimage), - }; - - if let Some(preimage) = payment_preimage { - self.channel_manager.claim_funds(preimage); - } else { - log_error!( - self.logger, - "Failed to claim payment with hash {}: preimage unknown.", - hex_utils::to_string(&payment_hash.0), - ); - self.channel_manager.fail_htlc_backwards(&payment_hash); - - let update = PaymentDetailsUpdate { - status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_id) - }; - self.payment_store.update(&update).unwrap_or_else(|e| { - log_error!(self.logger, "Failed to access payment store: {}", e); - panic!("Failed to access payment store"); - }); - } - }, - LdkEvent::PaymentClaimed { - payment_hash, - purpose, - amount_msat, - receiver_node_id: _, - htlcs: _, - sender_intended_total_msat: _, - } => { - log_info!( - self.logger, - "Claimed payment from payment hash {} of {}msat.", - hex_utils::to_string(&payment_hash.0), - amount_msat, - ); - let payment_id = PaymentId(payment_hash.0); - match purpose { - PaymentPurpose::Bolt11InvoicePayment { + PaymentPurpose::Bolt12OfferPayment { payment_preimage, payment_secret, + payment_context, .. } => { - let update = PaymentDetailsUpdate { - preimage: Some(payment_preimage), - secret: Some(Some(payment_secret)), - amount_msat: Some(Some(amount_msat)), - status: Some(PaymentStatus::Succeeded), - ..PaymentDetailsUpdate::new(payment_id) + let offer_id = payment_context.offer_id; + let payment = PaymentDetails { + id: payment_id, + kind: PaymentKind::Bolt12Offer { + hash: Some(payment_hash), + preimage: payment_preimage, + secret: Some(payment_secret), + offer_id, + }, + amount_msat: Some(amount_msat), + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, }; - match self.payment_store.update(&update) { - Ok(true) => (), - Ok(false) => { + + match self.payment_store.insert(payment) { + Ok(false) => (), + Ok(true) => { log_error!( self.logger, - "Payment with hash {} couldn't be found in store", - hex_utils::to_string(&payment_hash.0) + "Bolt12OfferPayment with ID {} was previously known", + payment_id, ); debug_assert!(false); }, Err(e) => { log_error!( self.logger, - "Failed to update payment with hash {}: {}", - hex_utils::to_string(&payment_hash.0), + "Failed to insert payment with ID {}: {}", + payment_id, e ); debug_assert!(false); }, } + payment_preimage }, - PaymentPurpose::Bolt12OfferPayment { .. } => { - // TODO: support BOLT12. - log_error!( - self.logger, - "Failed to claim unsupported BOLT12 payment with hash: {}", - payment_hash - ); - return; - }, - PaymentPurpose::Bolt12RefundPayment { .. } => { - // TODO: support BOLT12. - log_error!( - self.logger, - "Failed to claim unsupported BOLT12 payment with hash: {}", - payment_hash - ); - return; + PaymentPurpose::Bolt12RefundPayment { payment_preimage, .. } => { + payment_preimage }, PaymentPurpose::SpontaneousPayment(preimage) => { + // Since it's spontaneous, we insert it now into our store. let payment = PaymentDetails { id: payment_id, kind: PaymentKind::Spontaneous { @@ -623,7 +562,7 @@ where }, amount_msat: Some(amount_msat), direction: PaymentDirection::Inbound, - status: PaymentStatus::Succeeded, + status: PaymentStatus::Pending, }; match self.payment_store.insert(payment) { @@ -631,24 +570,125 @@ where Ok(true) => { log_error!( self.logger, - "Spontaneous payment with hash {} was previously known", - hex_utils::to_string(&payment_hash.0) + "Spontaneous payment with ID {} was previously known", + payment_id, ); debug_assert!(false); }, Err(e) => { log_error!( self.logger, - "Failed to insert payment with hash {}: {}", - hex_utils::to_string(&payment_hash.0), + "Failed to insert payment with ID {}: {}", + payment_id, e ); debug_assert!(false); }, } + + Some(preimage) }, }; + if let Some(preimage) = payment_preimage { + self.channel_manager.claim_funds(preimage); + } else { + log_error!( + self.logger, + "Failed to claim payment with ID {}: preimage unknown.", + payment_id, + ); + self.channel_manager.fail_htlc_backwards(&payment_hash); + + let update = PaymentDetailsUpdate { + hash: Some(Some(payment_hash)), + status: Some(PaymentStatus::Failed), + ..PaymentDetailsUpdate::new(payment_id) + }; + self.payment_store.update(&update).unwrap_or_else(|e| { + log_error!(self.logger, "Failed to access payment store: {}", e); + panic!("Failed to access payment store"); + }); + } + }, + LdkEvent::PaymentClaimed { + payment_hash, + purpose, + amount_msat, + receiver_node_id: _, + htlcs: _, + sender_intended_total_msat: _, + } => { + let payment_id = PaymentId(payment_hash.0); + log_info!( + self.logger, + "Claimed payment with ID {} from payment hash {} of {}msat.", + payment_id, + hex_utils::to_string(&payment_hash.0), + amount_msat, + ); + + let update = match purpose { + PaymentPurpose::Bolt11InvoicePayment { + payment_preimage, + payment_secret, + .. + } => PaymentDetailsUpdate { + preimage: Some(payment_preimage), + secret: Some(Some(payment_secret)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }, + PaymentPurpose::Bolt12OfferPayment { + payment_preimage, payment_secret, .. + } => PaymentDetailsUpdate { + preimage: Some(payment_preimage), + secret: Some(Some(payment_secret)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }, + PaymentPurpose::Bolt12RefundPayment { + payment_preimage, + payment_secret, + .. + } => PaymentDetailsUpdate { + preimage: Some(payment_preimage), + secret: Some(Some(payment_secret)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }, + PaymentPurpose::SpontaneousPayment(preimage) => PaymentDetailsUpdate { + preimage: Some(Some(preimage)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }, + }; + + match self.payment_store.update(&update) { + Ok(true) => (), + Ok(false) => { + log_error!( + self.logger, + "Payment with ID {} couldn't be found in store", + payment_id, + ); + debug_assert!(false); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to update payment with ID {}: {}", + payment_id, + e + ); + panic!("Failed to access payment store"); + }, + } + self.event_queue .add_event(Event::PaymentReceived { payment_id: Some(payment_id), @@ -675,6 +715,7 @@ where }; let update = PaymentDetailsUpdate { + hash: Some(Some(payment_hash)), preimage: Some(Some(payment_preimage)), status: Some(PaymentStatus::Succeeded), ..PaymentDetailsUpdate::new(payment_id) @@ -721,6 +762,7 @@ where ); let update = PaymentDetailsUpdate { + hash: Some(Some(payment_hash)), status: Some(PaymentStatus::Failed), ..PaymentDetailsUpdate::new(payment_id) }; @@ -977,8 +1019,49 @@ where LdkEvent::DiscardFunding { .. } => {}, LdkEvent::HTLCIntercepted { .. } => {}, LdkEvent::BumpTransaction(_) => {}, - LdkEvent::InvoiceRequestFailed { .. } => {}, - LdkEvent::ConnectionNeeded { .. } => {}, + LdkEvent::InvoiceRequestFailed { payment_id } => { + log_error!( + self.logger, + "Failed to request invoice for outbound BOLT12 payment {}", + payment_id + ); + let update = PaymentDetailsUpdate { + status: Some(PaymentStatus::Failed), + ..PaymentDetailsUpdate::new(payment_id) + }; + self.payment_store.update(&update).unwrap_or_else(|e| { + log_error!(self.logger, "Failed to access payment store: {}", e); + panic!("Failed to access payment store"); + }); + return; + }, + LdkEvent::ConnectionNeeded { node_id, addresses } => { + let runtime_lock = self.runtime.read().unwrap(); + debug_assert!(runtime_lock.is_some()); + + if let Some(runtime) = runtime_lock.as_ref() { + let spawn_logger = self.logger.clone(); + let spawn_cm = Arc::clone(&self.connection_manager); + runtime.spawn(async move { + for addr in &addresses { + match spawn_cm.connect_peer_if_necessary(node_id, addr.clone()).await { + Ok(()) => { + return; + }, + Err(e) => { + log_error!( + spawn_logger, + "Failed to establish connection to peer {}@{}: {}", + node_id, + addr, + e + ); + }, + } + } + }); + } + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 3d619cebb..9a634c12d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,7 +130,7 @@ use event::{EventHandler, EventQueue}; use gossip::GossipSource; use liquidity::LiquiditySource; use payment::store::PaymentStore; -use payment::{Bolt11Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; +use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; use peer_store::{PeerInfo, PeerStore}; use types::{ Broadcaster, ChainMonitor, ChannelManager, DynStore, FeeEstimator, KeysManager, NetworkGraph, @@ -541,7 +541,10 @@ impl Node { let mut stop_bcast = self.stop_sender.subscribe(); runtime.spawn(async move { // We check every 30 secs whether our last broadcast is NODE_ANN_BCAST_INTERVAL away. + #[cfg(not(test))] let mut interval = tokio::time::interval(Duration::from_secs(30)); + #[cfg(test)] + let mut interval = tokio::time::interval(Duration::from_secs(5)); loop { tokio::select! { _ = stop_bcast.changed() => { @@ -621,6 +624,7 @@ impl Node { Arc::clone(&self.event_queue), Arc::clone(&self.wallet), Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), Arc::clone(&self.payment_store), @@ -845,6 +849,32 @@ impl Node { )) } + /// Returns a payment handler allowing to create and pay [BOLT 12] offers and refunds. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + #[cfg(not(feature = "uniffi"))] + pub fn bolt12_payment(&self) -> Arc { + Arc::new(Bolt12Payment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.payment_store), + Arc::clone(&self.logger), + )) + } + + /// Returns a payment handler allowing to create and pay [BOLT 12] offers and refunds. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + #[cfg(feature = "uniffi")] + pub fn bolt12_payment(&self) -> Arc { + Arc::new(Bolt12Payment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.payment_store), + Arc::clone(&self.logger), + )) + } + /// Returns a payment handler allowing to send spontaneous ("keysend") payments. #[cfg(not(feature = "uniffi"))] pub fn spontaneous_payment(&self) -> SpontaneousPayment { diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs new file mode 100644 index 000000000..35fa3cfb4 --- /dev/null +++ b/src/payment/bolt12.rs @@ -0,0 +1,348 @@ +//! Holds a payment handler allowing to create and pay [BOLT 12] offers and refunds. +//! +//! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + +use crate::config::LDK_PAYMENT_RETRY_TIMEOUT; +use crate::error::Error; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::payment::store::{ + PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, +}; +use crate::types::ChannelManager; + +use lightning::ln::channelmanager::{PaymentId, Retry}; +use lightning::offers::invoice::Bolt12Invoice; +use lightning::offers::offer::{Amount, Offer}; +use lightning::offers::parse::Bolt12SemanticError; +use lightning::offers::refund::Refund; + +use rand::RngCore; + +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +/// A payment handler allowing to create and pay [BOLT 12] offers and refunds. +/// +/// Should be retrieved by calling [`Node::bolt12_payment`]. +/// +/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md +/// [`Node::bolt12_payment`]: crate::Node::bolt12_payment +pub struct Bolt12Payment { + runtime: Arc>>, + channel_manager: Arc, + payment_store: Arc>>, + logger: Arc, +} + +impl Bolt12Payment { + pub(crate) fn new( + runtime: Arc>>, + channel_manager: Arc, + payment_store: Arc>>, logger: Arc, + ) -> Self { + Self { runtime, channel_manager, payment_store, logger } + } + + /// Send a payment given an offer. + /// + /// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice + /// response. + pub fn send(&self, offer: &Offer, payer_note: Option) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let quantity = None; + let mut random_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut random_bytes); + let payment_id = PaymentId(random_bytes); + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let max_total_routing_fee_msat = None; + + let offer_amount_msat = match offer.amount() { + Some(Amount::Bitcoin { amount_msats }) => amount_msats, + Some(_) => { + log_error!(self.logger, "Failed to send payment as the provided offer was denominated in an unsupported currency."); + return Err(Error::UnsupportedCurrency); + }, + None => { + log_error!(self.logger, "Failed to send payment due to the given offer being \"zero-amount\". Please use send_using_amount instead."); + return Err(Error::InvalidOffer); + }, + }; + + match self.channel_manager.pay_for_offer( + &offer, + quantity, + None, + payer_note, + payment_id, + retry_strategy, + max_total_routing_fee_msat, + ) { + Ok(()) => { + let payee_pubkey = offer.signing_pubkey(); + log_info!( + self.logger, + "Initiated sending {}msat to {:?}", + offer_amount_msat, + payee_pubkey + ); + + let kind = PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: offer.id(), + }; + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: Some(*offer_amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Pending, + }; + self.payment_store.insert(payment)?; + + Ok(payment_id) + }, + Err(e) => { + log_error!(self.logger, "Failed to send invoice request: {:?}", e); + match e { + Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment), + _ => { + let kind = PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: offer.id(), + }; + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: Some(*offer_amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Failed, + }; + self.payment_store.insert(payment)?; + Err(Error::InvoiceRequestCreationFailed) + }, + } + }, + } + } + + /// Send a payment given an offer and an amount in millisatoshi. + /// + /// This will fail if the amount given is less than the value required by the given offer. + /// + /// This can be used to pay a so-called "zero-amount" offers, i.e., an offer that leaves the + /// amount paid to be determined by the user. + /// + /// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice + /// response. + pub fn send_using_amount( + &self, offer: &Offer, payer_note: Option, amount_msat: u64, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let quantity = None; + let mut random_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut random_bytes); + let payment_id = PaymentId(random_bytes); + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let max_total_routing_fee_msat = None; + + let offer_amount_msat = match offer.amount() { + Some(Amount::Bitcoin { amount_msats }) => *amount_msats, + Some(_) => { + log_error!(self.logger, "Failed to send payment as the provided offer was denominated in an unsupported currency."); + return Err(Error::UnsupportedCurrency); + }, + None => amount_msat, + }; + + if amount_msat < offer_amount_msat { + log_error!( + self.logger, + "Failed to pay as the given amount needs to be at least the offer amount: required {}msat, gave {}msat.", offer_amount_msat, amount_msat); + return Err(Error::InvalidAmount); + } + + match self.channel_manager.pay_for_offer( + &offer, + quantity, + Some(amount_msat), + payer_note, + payment_id, + retry_strategy, + max_total_routing_fee_msat, + ) { + Ok(()) => { + let payee_pubkey = offer.signing_pubkey(); + log_info!( + self.logger, + "Initiated sending {}msat to {:?}", + amount_msat, + payee_pubkey + ); + + let kind = PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: offer.id(), + }; + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: Some(amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Pending, + }; + self.payment_store.insert(payment)?; + + Ok(payment_id) + }, + Err(e) => { + log_error!(self.logger, "Failed to send payment: {:?}", e); + match e { + Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment), + _ => { + let kind = PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: offer.id(), + }; + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: Some(amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Failed, + }; + self.payment_store.insert(payment)?; + Err(Error::PaymentSendingFailed) + }, + } + }, + } + } + + /// Returns a payable offer that can be used to request and receive a payment of the amount + /// given. + pub fn receive(&self, amount_msat: u64, description: &str) -> Result { + let offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + let offer = offer_builder + .amount_msats(amount_msat) + .description(description.to_string()) + .build() + .map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + })?; + + Ok(offer) + } + + /// Returns a payable offer that can be used to request and receive a payment for which the + /// amount is to be determined by the user, also known as a "zero-amount" offer. + pub fn receive_variable_amount(&self, description: &str) -> Result { + let offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + let offer = offer_builder.description(description.to_string()).build().map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + })?; + + Ok(offer) + } + + /// Requests a refund payment for the given [`Refund`]. + /// + /// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to + /// retrieve the refund). + pub fn request_refund_payment(&self, refund: &Refund) -> Result { + let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| { + log_error!(self.logger, "Failed to request refund payment: {:?}", e); + Error::InvoiceRequestCreationFailed + })?; + + let payment_hash = invoice.payment_hash(); + let payment_id = PaymentId(payment_hash.0); + + let payment = PaymentDetails { + id: payment_id, + kind: PaymentKind::Bolt12Refund { + hash: Some(payment_hash), + preimage: None, + secret: None, + }, + amount_msat: Some(refund.amount_msats()), + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + }; + + self.payment_store.insert(payment)?; + + Ok(invoice) + } + + /// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given. + pub fn initiate_refund(&self, amount_msat: u64, expiry_secs: u32) -> Result { + let mut random_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut random_bytes); + let payment_id = PaymentId(random_bytes); + + let expiration = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) + .duration_since(UNIX_EPOCH) + .unwrap(); + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let max_total_routing_fee_msat = None; + + let refund = self + .channel_manager + .create_refund_builder( + amount_msat, + expiration, + payment_id, + retry_strategy, + max_total_routing_fee_msat, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to create refund builder: {:?}", e); + Error::RefundCreationFailed + })? + .build() + .map_err(|e| { + log_error!(self.logger, "Failed to create refund: {:?}", e); + Error::RefundCreationFailed + })?; + + log_info!(self.logger, "Offering refund of {}msat", amount_msat); + + let kind = PaymentKind::Bolt12Refund { hash: None, preimage: None, secret: None }; + + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: Some(amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Pending, + }; + + self.payment_store.insert(payment)?; + + Ok(refund) + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 3649f1fcc..1862bf2df 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -1,11 +1,13 @@ //! Objects for different types of payments. mod bolt11; +mod bolt12; mod onchain; mod spontaneous; pub(crate) mod store; pub use bolt11::Bolt11Payment; +pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; pub use spontaneous::SpontaneousPayment; pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; diff --git a/src/payment/store.rs b/src/payment/store.rs index 8bcbf41e6..f7f4942be 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -9,6 +9,7 @@ use crate::Error; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, @@ -145,7 +146,6 @@ pub enum PaymentKind { /// A [BOLT 11] payment. /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md - // TODO: Bolt11 { invoice: Option }, Bolt11 { /// The payment hash, i.e., the hash of the `preimage`. hash: PaymentHash, @@ -158,7 +158,6 @@ pub enum PaymentKind { /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md /// [LSPS 2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - // TODO: Bolt11Jit { invoice: Option }, Bolt11Jit { /// The payment hash, i.e., the hash of the `preimage`. hash: PaymentHash, @@ -176,6 +175,32 @@ pub enum PaymentKind { /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs lsp_fee_limits: LSPFeeLimits, }, + /// A [BOLT 12] 'offer' payment, i.e., a payment for an [`Offer`]. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + /// [`Offer`]: crate::lightning::offers::offer::Offer + Bolt12Offer { + /// The payment hash, i.e., the hash of the `preimage`. + hash: Option, + /// The pre-image used by the payment. + preimage: Option, + /// The secret used by the payment. + secret: Option, + /// The ID of the offer this payment is for. + offer_id: OfferId, + }, + /// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`]. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + /// [`Refund`]: lightning::offers::refund::Refund + Bolt12Refund { + /// The payment hash, i.e., the hash of the `preimage`. + hash: Option, + /// The pre-image used by the payment. + preimage: Option, + /// The secret used by the payment. + secret: Option, + }, /// A spontaneous ("keysend") payment. Spontaneous { /// The payment hash, i.e., the hash of the `preimage`. @@ -198,9 +223,20 @@ impl_writeable_tlv_based_enum!(PaymentKind, (4, secret, option), (6, lsp_fee_limits, required), }, + (6, Bolt12Offer) => { + (0, hash, option), + (2, preimage, option), + (4, secret, option), + (6, offer_id, required), + }, (8, Spontaneous) => { (0, hash, required), (2, preimage, option), + }, + (10, Bolt12Refund) => { + (0, hash, option), + (2, preimage, option), + (4, secret, option), }; ); @@ -227,6 +263,7 @@ impl_writeable_tlv_based!(LSPFeeLimits, { #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct PaymentDetailsUpdate { pub id: PaymentId, + pub hash: Option>, pub preimage: Option>, pub secret: Option>, pub amount_msat: Option>, @@ -236,7 +273,15 @@ pub(crate) struct PaymentDetailsUpdate { impl PaymentDetailsUpdate { pub fn new(id: PaymentId) -> Self { - Self { id, preimage: None, secret: None, amount_msat: None, direction: None, status: None } + Self { + id, + hash: None, + preimage: None, + secret: None, + amount_msat: None, + direction: None, + status: None, + } } } @@ -299,10 +344,30 @@ where let mut locked_payments = self.payments.lock().unwrap(); if let Some(payment) = locked_payments.get_mut(&update.id) { + if let Some(hash_opt) = update.hash { + match payment.kind { + PaymentKind::Bolt12Offer { ref mut hash, .. } => { + debug_assert_eq!(payment.direction, PaymentDirection::Outbound, + "We should only ever override payment hash for outbound BOLT 12 payments"); + *hash = hash_opt + }, + PaymentKind::Bolt12Refund { ref mut hash, .. } => { + debug_assert_eq!(payment.direction, PaymentDirection::Outbound, + "We should only ever override payment hash for outbound BOLT 12 payments"); + *hash = hash_opt + }, + _ => { + // We can omit updating the hash for BOLT11 payments as the payment hash + // will always be known from the beginning. + }, + } + } if let Some(preimage_opt) = update.preimage { match payment.kind { PaymentKind::Bolt11 { ref mut preimage, .. } => *preimage = preimage_opt, PaymentKind::Bolt11Jit { ref mut preimage, .. } => *preimage = preimage_opt, + PaymentKind::Bolt12Offer { ref mut preimage, .. } => *preimage = preimage_opt, + PaymentKind::Bolt12Refund { ref mut preimage, .. } => *preimage = preimage_opt, PaymentKind::Spontaneous { ref mut preimage, .. } => *preimage = preimage_opt, _ => {}, } @@ -312,6 +377,8 @@ where match payment.kind { PaymentKind::Bolt11 { ref mut secret, .. } => *secret = secret_opt, PaymentKind::Bolt11Jit { ref mut secret, .. } => *secret = secret_opt, + PaymentKind::Bolt12Offer { ref mut secret, .. } => *secret = secret_opt, + PaymentKind::Bolt12Refund { ref mut secret, .. } => *secret = secret_opt, _ => {}, } } @@ -327,7 +394,6 @@ where self.persist_info(&update.id, payment)?; updated = true; } - Ok(updated) } diff --git a/src/types.rs b/src/types.rs index 68ed36361..14d8adf76 100644 --- a/src/types.rs +++ b/src/types.rs @@ -115,7 +115,7 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - IgnoringMessageHandler, + Arc, IgnoringMessageHandler, >; diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index e979b5ce9..99e72e31c 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -2,6 +2,9 @@ pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, Pay pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; +pub use lightning::offers::invoice::Bolt12Invoice; +pub use lightning::offers::offer::{Offer, OfferId}; +pub use lightning::offers::refund::Refund; pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; @@ -20,6 +23,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::PaymentId; +use lightning::util::ser::Writeable; use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; @@ -75,6 +79,65 @@ impl UniffiCustomTypeConverter for Bolt11Invoice { } } +impl UniffiCustomTypeConverter for Offer { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Offer::from_str(&val).map_err(|_| Error::InvalidOffer.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Refund { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Refund::from_str(&val).map_err(|_| Error::InvalidRefund.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Bolt12Invoice { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + if let Ok(invoice) = Bolt12Invoice::try_from(bytes_vec) { + return Ok(invoice); + } + } + Err(Error::InvalidInvoice.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.encode()) + } +} + +impl UniffiCustomTypeConverter for OfferId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(OfferId(bytes)); + } + } + Err(Error::InvalidOfferId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + impl UniffiCustomTypeConverter for PaymentId { type Builtin = String; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index bcb47accb..44c8efde9 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,7 +2,7 @@ #![allow(dead_code)] use ldk_node::io::sqlite_store::SqliteStore; -use ldk_node::payment::{PaymentDirection, PaymentStatus}; +use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{Builder, Config, Event, LogLevel, Node, NodeError}; use lightning::ln::msgs::SocketAddress; @@ -102,7 +102,9 @@ macro_rules! expect_payment_successful_event { match $node.wait_next_event() { ref e @ Event::PaymentSuccessful { payment_id, fee_paid_msat, .. } => { println!("{} got event {:?}", $node.node_id(), e); - assert_eq!(fee_paid_msat, $fee_paid_msat); + if let Some(fee_msat) = $fee_paid_msat { + assert_eq!(fee_paid_msat, fee_msat); + } assert_eq!(payment_id, $payment_id); $node.event_handled(); }, @@ -445,9 +447,11 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert!(matches!(node_a.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); // Assert we fail duplicate outbound payments and check the status hasn't changed. assert_eq!(Err(NodeError::DuplicatePayment), node_a.bolt11_payment().send(&invoice)); @@ -490,9 +494,11 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); + assert!(matches!(node_a.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); + assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); // Test "zero-amount" invoice payment println!("\nB receive_variable_amount_payment"); @@ -524,9 +530,11 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); + assert!(matches!(node_a.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); + assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); // Test spontaneous/keysend payments println!("\nA send_spontaneous_payment"); @@ -548,9 +556,19 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_a.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Outbound); assert_eq!(node_a.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); + assert!(matches!( + node_a.payment(&keysend_payment_id).unwrap().kind, + PaymentKind::Spontaneous { .. } + )); assert_eq!(node_b.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_b.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Inbound); assert_eq!(node_b.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); + assert!(matches!( + node_b.payment(&keysend_payment_id).unwrap().kind, + PaymentKind::Spontaneous { .. } + )); + assert_eq!(node_a.list_payments().len(), 4); + assert_eq!(node_b.list_payments().len(), 5); println!("\nB close_channel"); node_b.close_channel(&user_channel_id, node_a.node_id()).unwrap(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 1820ef76a..fa1b33b39 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -7,14 +7,18 @@ use common::{ setup_node, setup_two_nodes, wait_for_tx, TestSyncStore, }; +use ldk_node::payment::PaymentKind; use ldk_node::{Builder, Event, NodeError}; +use lightning::ln::channelmanager::PaymentId; use lightning::util::persist::KVStore; use bitcoin::{Amount, Network}; use std::sync::Arc; +use crate::common::expect_channel_ready_event; + #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -138,7 +142,7 @@ fn multi_hop_sending() { let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); let fee_paid_msat = Some(2000); - expect_payment_successful_event!(nodes[0], payment_id, fee_paid_msat); + expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat)); } #[test] @@ -366,3 +370,164 @@ fn concurrent_connections_succeed() { h.join().unwrap(); } } + +#[test] +fn simple_bolt12_send_receive() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_nodes(&electrsd, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_amount_sat), + ); + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Sleep until we broadcasted a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); + + let expected_amount_msat = 100_000_000; + let offer = node_b.bolt12_payment().receive(expected_amount_msat, "asdf").unwrap(); + let payment_id = node_a.bolt12_payment().send(&offer, None).unwrap(); + + expect_payment_successful_event!(node_a, Some(payment_id), None); + let node_a_payments = node_a.list_payments(); + assert_eq!(node_a_payments.len(), 1); + match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret: _, offer_id } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(offer_id, offer.id()); + //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + //API currently doesn't allow to do that. + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + expect_payment_received_event!(node_b, expected_amount_msat); + let node_b_payments = node_b.list_payments(); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + assert_eq!(offer_id, offer.id()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + // Test send_using_amount + let offer_amount_msat = 100_000_000; + let less_than_offer_amount = offer_amount_msat - 10_000; + let expected_amount_msat = offer_amount_msat + 10_000; + let offer = node_b.bolt12_payment().receive(offer_amount_msat, "asdf").unwrap(); + assert!(node_a + .bolt12_payment() + .send_using_amount(&offer, None, less_than_offer_amount) + .is_err()); + let payment_id = + node_a.bolt12_payment().send_using_amount(&offer, None, expected_amount_msat).unwrap(); + + expect_payment_successful_event!(node_a, Some(payment_id), None); + let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id); + assert_eq!(node_a_payments.len(), 1); + let payment_hash = match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret: _, offer_id } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(offer_id, offer.id()); + //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + //API currently doesn't allow to do that. + hash.unwrap() + }, + _ => { + panic!("Unexpected payment kind"); + }, + }; + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + expect_payment_received_event!(node_b, expected_amount_msat); + let node_b_payment_id = PaymentId(payment_hash.0); + let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + assert_eq!(offer_id, offer.id()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + // Now node_b refunds the amount node_a just overpaid. + let overpaid_amount = expected_amount_msat - offer_amount_msat; + let refund = node_b.bolt12_payment().initiate_refund(overpaid_amount, 3600).unwrap(); + let invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); + expect_payment_received_event!(node_a, overpaid_amount); + + let node_b_payment_id = node_b + .list_payments_with_filter(|p| p.amount_msat == Some(overpaid_amount)) + .first() + .unwrap() + .id; + expect_payment_successful_event!(node_b, Some(node_b_payment_id), None); + + let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Refund { hash, preimage, secret: _ } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + //API currently doesn't allow to do that. + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount)); + + let node_a_payment_id = PaymentId(invoice.payment_hash().0); + let node_a_payments = node_a.list_payments_with_filter(|p| p.id == node_a_payment_id); + assert_eq!(node_a_payments.len(), 1); + match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Refund { hash, preimage, secret } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); +}