Skip to content

Commit

Permalink
Introduce Bolt12Payment API
Browse files Browse the repository at this point in the history
  • Loading branch information
tnull committed Mar 12, 2024
1 parent c194be1 commit cd6289e
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 1 deletion.
18 changes: 18 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface Node {
PublicKey node_id();
sequence<SocketAddress>? listening_addresses();
Bolt11Payment bolt11_payment();
Bolt12Payment bolt12_payment();
SpontaneousPayment spontaneous_payment();
OnchainPayment onchain_payment();
[Throws=NodeError]
Expand Down Expand Up @@ -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);
Expand All @@ -122,6 +134,7 @@ enum NodeError {
"OnchainTxCreationFailed",
"ConnectionFailed",
"InvoiceCreationFailed",
"OfferCreationFailed",
"PaymentSendingFailed",
"ProbeSendingFailed",
"ChannelCreationFailed",
Expand All @@ -145,9 +158,11 @@ enum NodeError {
"InvalidPaymentSecret",
"InvalidAmount",
"InvalidInvoice",
"InvalidOffer",
"InvalidChannelId",
"InvalidNetwork",
"DuplicatePayment",
"UnsupportedCurrency",
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
Expand Down Expand Up @@ -371,6 +386,9 @@ typedef string Address;
[Custom]
typedef string Bolt11Invoice;

[Custom]
typedef string Offer;

[Custom]
typedef string PaymentId;

Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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."),
Expand All @@ -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 => {
Expand All @@ -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.")
},
Expand Down
28 changes: 27 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Bolt12Payment> {
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<Bolt12Payment> {
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 {
Expand Down
242 changes: 242 additions & 0 deletions src/payment/bolt12.rs
Original file line number Diff line number Diff line change
@@ -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<RwLock<Option<tokio::runtime::Runtime>>>,
channel_manager: Arc<ChannelManager>,
payment_store: Arc<PaymentStore<Arc<FilesystemLogger>>>,
logger: Arc<FilesystemLogger>,
}

impl Bolt12Payment {
pub(crate) fn new(
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
channel_manager: Arc<ChannelManager>,
payment_store: Arc<PaymentStore<Arc<FilesystemLogger>>>, logger: Arc<FilesystemLogger>,
) -> 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<String>) -> Result<PaymentId, Error> {
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<String>, amount_msat: u64,
) -> Result<PaymentId, Error> {
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<Offer, Error> {
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<Offer, Error> {
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)
}
}
Loading

0 comments on commit cd6289e

Please sign in to comment.