Skip to content

Commit ac99cf6

Browse files
committed
Adopt LDK's pay_for_offer_from_hrn method
Adopt the upstream LDK change to use the dedicated pay_for_offer_from_hrn method when initiating a payment for an offer resolved via a Human-Readable Name (HRN). This ensures better alignment with LDK best practices.
1 parent cac4b45 commit ac99cf6

File tree

5 files changed

+174
-51
lines changed

5 files changed

+174
-51
lines changed

bindings/ldk_node.udl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ interface Bolt12Payment {
216216
[Throws=NodeError]
217217
PaymentId send([ByRef]Offer offer, u64? quantity, string? payer_note, RouteParametersConfig? route_parameters);
218218
[Throws=NodeError]
219-
PaymentId send_using_amount([ByRef]Offer offer, u64 amount_msat, u64? quantity, string? payer_note, RouteParametersConfig? route_parameters);
219+
PaymentId send_using_amount([ByRef]Offer offer, u64 amount_msat, u64? quantity, string? payer_note, RouteParametersConfig? route_parameters, HumanReadableName? hrn);
220220
[Throws=NodeError]
221221
Offer receive(u64 amount_msat, [ByRef]string description, u32? expiry_secs, u64? quantity);
222222
[Throws=NodeError]
@@ -336,6 +336,7 @@ enum NodeError {
336336
"LiquidityFeeTooHigh",
337337
"InvalidBlindedPaths",
338338
"AsyncPaymentServicesDisabled",
339+
"HrnParsingFailed",
339340
};
340341

341342
dictionary NodeStatus {
@@ -799,6 +800,13 @@ interface Offer {
799800
PublicKey? issuer_signing_pubkey();
800801
};
801802

803+
interface HumanReadableName {
804+
[Throws=NodeError, Name=from_encoded]
805+
constructor([ByRef] string encoded);
806+
string user();
807+
string domain();
808+
};
809+
802810
[Traits=(Debug, Display, Eq)]
803811
interface Refund {
804812
[Throws=NodeError, Name=from_str]

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ pub enum Error {
127127
InvalidBlindedPaths,
128128
/// Asynchronous payment services are disabled.
129129
AsyncPaymentServicesDisabled,
130+
/// Parsing a Human-Readable Name has failed.
131+
HrnParsingFailed,
130132
}
131133

132134
impl fmt::Display for Error {
@@ -205,6 +207,9 @@ impl fmt::Display for Error {
205207
Self::AsyncPaymentServicesDisabled => {
206208
write!(f, "Asynchronous payment services are disabled.")
207209
},
210+
Self::HrnParsingFailed => {
211+
write!(f, "Failed to parse a human-readable name.")
212+
},
208213
}
209214
}
210215
}

src/ffi/types.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub use crate::payment::store::{
5656
};
5757
pub use crate::payment::UnifiedPaymentResult;
5858

59-
pub use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
59+
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
6060

6161
use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId};
6262

@@ -270,6 +270,72 @@ impl std::fmt::Display for Offer {
270270
}
271271
}
272272

273+
/// A struct containing the two parts of a BIP 353 Human-Readable Name - the user and domain parts.
274+
///
275+
/// The `user` and `domain` parts combined cannot exceed 231 bytes in length;
276+
/// each DNS label within them must be non-empty and no longer than 63 bytes.
277+
///
278+
/// If you intend to handle non-ASCII `user` or `domain` parts, you must handle [Homograph Attacks]
279+
/// and do punycode en-/de-coding yourself. This struct will always handle only plain ASCII `user`
280+
/// and `domain` parts.
281+
///
282+
/// This struct can also be used for LN-Address recipients.
283+
///
284+
/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack
285+
pub struct HumanReadableName {
286+
pub(crate) inner: LdkHumanReadableName,
287+
}
288+
289+
impl HumanReadableName {
290+
/// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`.
291+
///
292+
/// If `user` includes the standard BIP 353 ₿ prefix it is automatically removed as required by
293+
/// BIP 353.
294+
pub fn from_encoded(encoded: &str) -> Result<Self, Error> {
295+
let hrn = match LdkHumanReadableName::from_encoded(encoded) {
296+
Ok(hrn) => Ok(hrn),
297+
Err(_) => Err(Error::HrnParsingFailed),
298+
}?;
299+
300+
Ok(Self { inner: hrn })
301+
}
302+
303+
/// Gets the `user` part of this Human-Readable Name
304+
pub fn user(&self) -> String {
305+
self.inner.user().to_string()
306+
}
307+
308+
/// Gets the `domain` part of this Human-Readable Name
309+
pub fn domain(&self) -> String {
310+
self.inner.domain().to_string()
311+
}
312+
}
313+
314+
impl From<LdkHumanReadableName> for HumanReadableName {
315+
fn from(ldk_hrn: LdkHumanReadableName) -> Self {
316+
HumanReadableName { inner: ldk_hrn }
317+
}
318+
}
319+
320+
impl From<HumanReadableName> for LdkHumanReadableName {
321+
fn from(wrapper: HumanReadableName) -> Self {
322+
wrapper.inner
323+
}
324+
}
325+
326+
impl Deref for HumanReadableName {
327+
type Target = LdkHumanReadableName;
328+
fn deref(&self) -> &Self::Target {
329+
&self.inner
330+
}
331+
}
332+
333+
impl AsRef<LdkHumanReadableName> for HumanReadableName {
334+
fn as_ref(&self) -> &LdkHumanReadableName {
335+
self.deref()
336+
}
337+
}
338+
273339
/// A `Refund` is a request to send an [`Bolt12Invoice`] without a preceding [`Offer`].
274340
///
275341
/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to

src/payment/bolt12.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
1515

1616
use lightning::blinded_path::message::BlindedMessagePath;
1717
use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId, Retry};
18-
use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity};
18+
use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity};
1919
use lightning::offers::parse::Bolt12SemanticError;
2020
use lightning::routing::router::RouteParametersConfig;
2121
#[cfg(feature = "uniffi")]
@@ -45,6 +45,11 @@ type Refund = lightning::offers::refund::Refund;
4545
#[cfg(feature = "uniffi")]
4646
type Refund = Arc<crate::ffi::Refund>;
4747

48+
#[cfg(not(feature = "uniffi"))]
49+
type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName;
50+
#[cfg(feature = "uniffi")]
51+
type HumanReadableName = Arc<crate::ffi::HumanReadableName>;
52+
4853
/// A payment handler allowing to create and pay [BOLT 12] offers and refunds.
4954
///
5055
/// Should be retrieved by calling [`Node::bolt12_payment`].
@@ -193,6 +198,37 @@ impl Bolt12Payment {
193198
pub fn send_using_amount(
194199
&self, offer: &Offer, amount_msat: u64, quantity: Option<u64>, payer_note: Option<String>,
195200
route_parameters: Option<RouteParametersConfig>,
201+
) -> Result<PaymentId, Error> {
202+
let payment_id = self.send_using_amount_inner(
203+
offer,
204+
amount_msat,
205+
quantity,
206+
payer_note,
207+
route_parameters,
208+
None,
209+
)?;
210+
Ok(payment_id)
211+
}
212+
213+
/// Internal helper to send a BOLT12 offer payment given an offer
214+
/// and an amount in millisatoshi.
215+
///
216+
/// This function contains the core payment logic and is called by
217+
/// [`Self::send_using_amount`] and other internal logic that resolves
218+
/// payment parameters (e.g. [`crate::UnifiedPayment::send`]).
219+
///
220+
/// It wraps the core LDK `pay_for_offer` logic and handles necessary pre-checks,
221+
/// payment ID generation, and payment details storage.
222+
///
223+
/// The amount validation logic ensures the provided `amount_msat` is sufficient
224+
/// based on the offer's required amount.
225+
///
226+
/// If `hrn` is `Some`, the payment is initiated using [`ChannelManager::pay_for_offer_from_hrn`]
227+
/// for offers resolved from a Human-Readable Name ([`HumanReadableName`]).
228+
/// Otherwise, it falls back to the standard offer payment methods.
229+
pub(crate) fn send_using_amount_inner(
230+
&self, offer: &Offer, amount_msat: u64, quantity: Option<u64>, payer_note: Option<String>,
231+
route_parameters: Option<RouteParametersConfig>, hrn: Option<HumanReadableName>,
196232
) -> Result<PaymentId, Error> {
197233
if !*self.is_running.read().unwrap() {
198234
return Err(Error::NotRunning);
@@ -228,7 +264,11 @@ impl Bolt12Payment {
228264
retry_strategy,
229265
route_params_config: route_parameters,
230266
};
231-
let res = if let Some(quantity) = quantity {
267+
let res = if let Some(hrn) = hrn {
268+
let hrn = maybe_deref(&hrn);
269+
let offer = OfferFromHrn { offer: offer.clone(), hrn: *hrn };
270+
self.channel_manager.pay_for_offer_from_hrn(&offer, amount_msat, payment_id, params)
271+
} else if let Some(quantity) = quantity {
232272
self.channel_manager.pay_for_offer_with_quantity(
233273
&offer,
234274
Some(amount_msat),

src/payment/unified.rs

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use std::vec::IntoIter;
2525

2626
use lightning::ln::channelmanager::PaymentId;
2727
use lightning::offers::offer::Offer;
28+
use lightning::onion_message::dns_resolution::HumanReadableName;
2829
use lightning::routing::router::RouteParametersConfig;
2930
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
3031

@@ -199,56 +200,59 @@ impl UnifiedPayment {
199200
},
200201
};
201202

202-
if let Some(PaymentMethod::LightningBolt12(offer)) =
203-
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt12(_)))
204-
{
205-
let offer = maybe_wrap(offer.clone());
206-
let payment_result = if let Some(amount_msat) = amount_msat {
207-
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
208-
} else {
209-
self.bolt12_payment.send(&offer, None, None, route_parameters)
210-
}
211-
.map_err(|e| {
212-
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
213-
e
214-
});
215-
216-
if let Ok(payment_id) = payment_result {
217-
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });
218-
}
219-
}
203+
for method in resolved.methods() {
204+
match method {
205+
PaymentMethod::LightningBolt12(offer) => {
206+
let offer = maybe_wrap(offer.clone());
207+
208+
let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
209+
let hrn = maybe_wrap(hrn.clone());
210+
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
211+
} else if let Some(amount_msat) = amount_msat {
212+
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
213+
} else {
214+
self.bolt12_payment.send(&offer, None, None, route_parameters)
215+
}
216+
.map_err(|e| {
217+
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
218+
e
219+
});
220220

221-
if let Some(PaymentMethod::LightningBolt11(invoice)) =
222-
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt11(_)))
223-
{
224-
let invoice = maybe_wrap(invoice.clone());
225-
let payment_result = self.bolt11_invoice.send(&invoice, route_parameters)
226-
.map_err(|e| {
227-
log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e);
228-
e
229-
});
230-
231-
if let Ok(payment_id) = payment_result {
232-
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
221+
if let Ok(payment_id) = payment_result {
222+
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });
223+
}
224+
},
225+
PaymentMethod::LightningBolt11(invoice) => {
226+
let invoice = maybe_wrap(invoice.clone());
227+
let payment_result = self.bolt11_invoice.send(&invoice, route_parameters)
228+
.map_err(|e| {
229+
log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e);
230+
e
231+
});
232+
233+
if let Ok(payment_id) = payment_result {
234+
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
235+
}
236+
},
237+
PaymentMethod::OnChain(address) => {
238+
let amount = resolved.onchain_payment_amount().ok_or_else(|| {
239+
log_error!(self.logger, "No amount specified. Aborting the payment.");
240+
Error::InvalidAmount
241+
})?;
242+
243+
let amt_sats = amount.sats().map_err(|_| {
244+
log_error!(
245+
self.logger,
246+
"Amount in sats returned an error. Aborting the payment."
247+
);
248+
Error::InvalidAmount
249+
})?;
250+
251+
let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?;
252+
return Ok(UnifiedPaymentResult::Onchain { txid });
253+
},
233254
}
234255
}
235-
236-
if let Some(PaymentMethod::OnChain(address)) =
237-
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::OnChain(_)))
238-
{
239-
let amount = resolved.onchain_payment_amount().ok_or_else(|| {
240-
log_error!(self.logger, "No amount specified. Aborting the payment.");
241-
Error::InvalidAmount
242-
})?;
243-
244-
let amt_sats = amount.sats().map_err(|_| {
245-
log_error!(self.logger, "Amount in sats returned an error. Aborting the payment.");
246-
Error::InvalidAmount
247-
})?;
248-
249-
let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?;
250-
return Ok(UnifiedPaymentResult::Onchain { txid });
251-
}
252256
log_error!(self.logger, "Payable methods not found in URI");
253257
Err(Error::PaymentSendingFailed)
254258
}

0 commit comments

Comments
 (0)