diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 69ea71f34..35cc133c0 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,17 @@ 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); +}; + interface SpontaneousPayment { [Throws=NodeError] PaymentId send(u64 amount_msat, PublicKey node_id); @@ -122,6 +134,7 @@ enum NodeError { "OnchainTxCreationFailed", "ConnectionFailed", "InvoiceCreationFailed", + "OfferCreationFailed", "PaymentSendingFailed", "ProbeSendingFailed", "ChannelCreationFailed", @@ -145,9 +158,11 @@ enum NodeError { "InvalidPaymentSecret", "InvalidAmount", "InvalidInvoice", + "InvalidOffer", "InvalidChannelId", "InvalidNetwork", "DuplicatePayment", + "UnsupportedCurrency", "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", @@ -371,6 +386,9 @@ typedef string Address; [Custom] typedef string Bolt11Invoice; +[Custom] +typedef string Offer; + [Custom] typedef string PaymentId; diff --git a/src/error.rs b/src/error.rs index 5acc75af8..e4fd7350e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,8 @@ pub enum Error { ConnectionFailed, /// Invoice creation failed. InvoiceCreationFailed, + /// Offer creation failed. + OfferCreationFailed, /// Sending a payment has failed. PaymentSendingFailed, /// Sending a payment probe has failed. @@ -59,12 +61,16 @@ pub enum Error { InvalidAmount, /// The given invoice is invalid. InvalidInvoice, + /// The given offer is invalid. + InvalidOffer, /// 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 +89,7 @@ impl fmt::Display for Error { }, Self::ConnectionFailed => write!(f, "Network connection closed."), Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."), + Self::OfferCreationFailed => write!(f, "Failed to create offer."), 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."), @@ -108,6 +115,7 @@ impl fmt::Display for Error { 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::InvalidChannelId => write!(f, "The given channel ID is invalid."), Self::InvalidNetwork => write!(f, "The given network is invalid."), Self::DuplicatePayment => { @@ -116,6 +124,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/lib.rs b/src/lib.rs index d79fa8aa7..9b522057a 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::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, @@ -846,6 +846,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..08ff16c12 --- /dev/null +++ b/src/payment/bolt12.rs @@ -0,0 +1,242 @@ +//! 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::payment_store::{ + PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, +}; +use crate::types::ChannelManager; + +use lightning::ln::channelmanager::{PaymentId, Retry}; +use lightning::offers::offer::{Amount, Offer}; +use lightning::offers::parse::Bolt12SemanticError; + +use rand::RngCore; + +use std::sync::{Arc, RwLock}; + +/// 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 amount_msats = 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, + _ => { + log_error!(self.logger, "Failed to send payment as the provided offer was denominated in an unsupported currency."); + return Err(Error::UnsupportedCurrency); + }, + }; + + match self.channel_manager.pay_for_offer( + &offer, + quantity, + amount_msats, + payer_note, + payment_id, + retry_strategy, + max_total_routing_fee_msat, + ) { + Ok(()) => { + let payee_pubkey = offer.signing_pubkey(); + log_info!( + self.logger, + "Initiated sending {:?} to {}", + offer_amount_msat, + payee_pubkey + ); + + let kind = PaymentKind::Bolt12 {}; + 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 payment: {:?}", e); + match e { + Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment), + _ => { + let kind = PaymentKind::Bolt12 {}; + 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::PaymentSendingFailed) + }, + } + }, + } + } + + /// 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 amount_msats = 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, + amount_msats, + payer_note, + payment_id, + retry_strategy, + max_total_routing_fee_msat, + ) { + Ok(()) => { + let payee_pubkey = offer.signing_pubkey(); + let offer_amount_msat = offer.amount(); + log_info!( + self.logger, + "Initiated sending {:?} to {}", + offer_amount_msat, + payee_pubkey + ); + + let kind = PaymentKind::Bolt12 {}; + 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::Bolt12 {}; + 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(description.to_string()).map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + let offer = offer_builder.amount_msats(amount_msat).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(description.to_string()).map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + let offer = offer_builder.build().map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + })?; + Ok(offer) + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 4799d5141..820c0e9ff 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; pub(crate) mod payment_store; mod spontaneous; pub use bolt11::Bolt11Payment; +pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; pub use payment_store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; pub use spontaneous::SpontaneousPayment; diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 43dd869d7..a8319fddb 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -4,6 +4,7 @@ pub use crate::payment::payment_store::{ pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; +pub use lightning::offers::offer::Offer; pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; @@ -77,6 +78,18 @@ 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 PaymentId { type Builtin = String;