Skip to content

Commit d47958a

Browse files
committed
Refactor unified_qr to use bitcoin-payment-instructions
Refactor the unified_qr.rs module into unified.rs to provide a single API for sending payments to BIP 21/321 URIs and BIP 353 HRNs. This change simplifies the user interface by leveraging the bitcoin-payment-instructions library for parsing. Key changes: - Rename UnifiedQrPayment to UnifiedPayment. - Rename QRPaymentResult to UnifiedPaymentResult. - Update the send method to support both URIs and HRNs. - Update integration tests to match the new unified flow.
1 parent bbefa73 commit d47958a

File tree

11 files changed

+344
-111
lines changed

11 files changed

+344
-111
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ log = { version = "0.4.22", default-features = false, features = ["std"]}
7777

7878
vss-client = { package = "vss-client-ng", version = "0.4" }
7979
prost = { version = "0.11.6", default-features = false}
80+
#bitcoin-payment-instructions = { version = "0.6" }
81+
bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", branch = "2025-12-ldk-node-base" }
8082

8183
[target.'cfg(windows)'.dependencies]
8284
winapi = { version = "0.3", features = ["winbase"] }

bindings/ldk_node.udl

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ interface Node {
149149
Bolt12Payment bolt12_payment();
150150
SpontaneousPayment spontaneous_payment();
151151
OnchainPayment onchain_payment();
152-
UnifiedQrPayment unified_qr_payment();
152+
UnifiedPayment unified_payment();
153153
LSPS1Liquidity lsps1_liquidity();
154154
[Throws=NodeError]
155155
void connect(PublicKey node_id, SocketAddress address, boolean persist);
@@ -275,11 +275,11 @@ interface FeeRate {
275275
u64 to_sat_per_vb_ceil();
276276
};
277277

278-
interface UnifiedQrPayment {
278+
interface UnifiedPayment {
279279
[Throws=NodeError]
280280
string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec);
281-
[Throws=NodeError]
282-
QrPaymentResult send([ByRef]string uri_str, RouteParametersConfig? route_parameters);
281+
[Throws=NodeError, Async]
282+
UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat, RouteParametersConfig? route_parameters);
283283
};
284284

285285
interface LSPS1Liquidity {
@@ -347,6 +347,7 @@ enum NodeError {
347347
"LiquidityFeeTooHigh",
348348
"InvalidBlindedPaths",
349349
"AsyncPaymentServicesDisabled",
350+
"HrnParsingFailed",
350351
};
351352

352353
dictionary NodeStatus {
@@ -456,7 +457,7 @@ interface PaymentKind {
456457
};
457458

458459
[Enum]
459-
interface QrPaymentResult {
460+
interface UnifiedPaymentResult {
460461
Onchain(Txid txid);
461462
Bolt11(PaymentId payment_id);
462463
Bolt12(PaymentId payment_id);
@@ -809,6 +810,13 @@ interface Offer {
809810
PublicKey? issuer_signing_pubkey();
810811
};
811812

813+
interface HumanReadableName {
814+
[Throws=NodeError, Name=from_encoded]
815+
constructor([ByRef] string encoded);
816+
string user();
817+
string domain();
818+
};
819+
812820
[Traits=(Debug, Display, Eq)]
813821
interface Refund {
814822
[Throws=NodeError, Name=from_str]

src/builder.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ use bitcoin::bip32::{ChildNumber, Xpriv};
1919
use bitcoin::key::Secp256k1;
2020
use bitcoin::secp256k1::PublicKey;
2121
use bitcoin::{BlockHash, Network};
22+
23+
use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver;
24+
2225
use lightning::chain::{chainmonitor, BestBlock, Watch};
2326
use lightning::io::Cursor;
2427
use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs};
@@ -1439,6 +1442,8 @@ fn build_with_store_internal(
14391442
})?;
14401443
}
14411444

1445+
let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph)));
1446+
14421447
// Initialize the PeerManager
14431448
let onion_messenger: Arc<OnionMessenger> =
14441449
if let Some(AsyncPaymentsRole::Server) = async_payments_role {
@@ -1450,7 +1455,7 @@ fn build_with_store_internal(
14501455
message_router,
14511456
Arc::clone(&channel_manager),
14521457
Arc::clone(&channel_manager),
1453-
IgnoringMessageHandler {},
1458+
Arc::clone(&hrn_resolver),
14541459
IgnoringMessageHandler {},
14551460
))
14561461
} else {
@@ -1462,7 +1467,7 @@ fn build_with_store_internal(
14621467
message_router,
14631468
Arc::clone(&channel_manager),
14641469
Arc::clone(&channel_manager),
1465-
IgnoringMessageHandler {},
1470+
Arc::clone(&hrn_resolver),
14661471
IgnoringMessageHandler {},
14671472
))
14681473
};
@@ -1594,6 +1599,12 @@ fn build_with_store_internal(
15941599
Arc::clone(&keys_manager),
15951600
));
15961601

1602+
let peer_manager_clone = Arc::clone(&peer_manager);
1603+
1604+
hrn_resolver.register_post_queue_action(Box::new(move || {
1605+
peer_manager_clone.process_events();
1606+
}));
1607+
15971608
liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::clone(&peer_manager)));
15981609

15991610
gossip_source.set_gossip_verifier(
@@ -1701,6 +1712,7 @@ fn build_with_store_internal(
17011712
node_metrics,
17021713
om_mailbox,
17031714
async_payments_role,
1715+
hrn_resolver,
17041716
})
17051717
}
17061718

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ pub enum Error {
129129
InvalidBlindedPaths,
130130
/// Asynchronous payment services are disabled.
131131
AsyncPaymentServicesDisabled,
132+
/// Parsing a Human-Readable Name has failed.
133+
HrnParsingFailed,
132134
}
133135

134136
impl fmt::Display for Error {
@@ -208,6 +210,9 @@ impl fmt::Display for Error {
208210
Self::AsyncPaymentServicesDisabled => {
209211
write!(f, "Asynchronous payment services are disabled.")
210212
},
213+
Self::HrnParsingFailed => {
214+
write!(f, "Failed to parse a human-readable name.")
215+
},
211216
}
212217
}
213218
}

src/ffi/types.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ pub use crate::logger::{LogLevel, LogRecord, LogWriter};
5555
pub use crate::payment::store::{
5656
ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus,
5757
};
58-
pub use crate::payment::QrPaymentResult;
58+
pub use crate::payment::UnifiedPaymentResult;
59+
60+
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
61+
5962
use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId};
6063

6164
impl UniffiCustomTypeConverter for PublicKey {
@@ -284,6 +287,72 @@ impl std::fmt::Display for Offer {
284287
}
285288
}
286289

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

src/lib.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,15 @@ use payment::asynchronous::om_mailbox::OnionMessageMailbox;
152152
use payment::asynchronous::static_invoice_store::StaticInvoiceStore;
153153
use payment::{
154154
Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment,
155-
UnifiedQrPayment,
155+
UnifiedPayment,
156156
};
157157
use peer_store::{PeerInfo, PeerStore};
158158
use rand::Rng;
159159
use runtime::Runtime;
160160
use types::{
161161
Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph,
162-
KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet,
162+
HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper,
163+
Wallet,
163164
};
164165
pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, SyncAndAsyncKVStore, UserChannelId};
165166
pub use {
@@ -206,6 +207,7 @@ pub struct Node {
206207
node_metrics: Arc<RwLock<NodeMetrics>>,
207208
om_mailbox: Option<Arc<OnionMessageMailbox>>,
208209
async_payments_role: Option<AsyncPaymentsRole>,
210+
hrn_resolver: Arc<HRNResolver>,
209211
}
210212

211213
impl Node {
@@ -945,34 +947,42 @@ impl Node {
945947
/// Returns a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11],
946948
/// and [BOLT 12] payment options.
947949
///
950+
/// This handler allows you to send payments to these URIs as well as [BIP 353] HRNs.
951+
///
948952
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
949953
/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
950954
/// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
955+
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki
951956
#[cfg(not(feature = "uniffi"))]
952-
pub fn unified_qr_payment(&self) -> UnifiedQrPayment {
953-
UnifiedQrPayment::new(
957+
pub fn unified_payment(&self) -> UnifiedPayment {
958+
UnifiedPayment::new(
954959
self.onchain_payment().into(),
955960
self.bolt11_payment().into(),
956961
self.bolt12_payment().into(),
957962
Arc::clone(&self.config),
958963
Arc::clone(&self.logger),
964+
Arc::clone(&self.hrn_resolver),
959965
)
960966
}
961967

962968
/// Returns a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11],
963969
/// and [BOLT 12] payment options.
964970
///
971+
/// This handler allows you to send payments to these URIs as well as [BIP 353] HRNs.
972+
///
965973
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
966974
/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
967975
/// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
976+
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki
968977
#[cfg(feature = "uniffi")]
969-
pub fn unified_qr_payment(&self) -> Arc<UnifiedQrPayment> {
970-
Arc::new(UnifiedQrPayment::new(
978+
pub fn unified_payment(&self) -> Arc<UnifiedPayment> {
979+
Arc::new(UnifiedPayment::new(
971980
self.onchain_payment(),
972981
self.bolt11_payment(),
973982
self.bolt12_payment(),
974983
Arc::clone(&self.config),
975984
Arc::clone(&self.logger),
985+
Arc::clone(&self.hrn_resolver),
976986
))
977987
}
978988

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/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod bolt12;
1313
mod onchain;
1414
mod spontaneous;
1515
pub(crate) mod store;
16-
mod unified_qr;
16+
mod unified;
1717

1818
pub use bolt11::Bolt11Payment;
1919
pub use bolt12::Bolt12Payment;
@@ -22,4 +22,4 @@ pub use spontaneous::SpontaneousPayment;
2222
pub use store::{
2323
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
2424
};
25-
pub use unified_qr::{QrPaymentResult, UnifiedQrPayment};
25+
pub use unified::{UnifiedPayment, UnifiedPaymentResult};

0 commit comments

Comments
 (0)