Skip to content

Commit a21ecc1

Browse files
committed
Support LNURL resolution via optional HTTP fallback
Add an `enable_lnurl_resolution` flag to `HumanReadableNamesConfig` that, when set, attaches an `HTTPHrnResolver` as a fallback to the configured BIP 353 / bLIP-32 resolver. The fallback is consulted only when the primary resolver returns an error, so it covers LN-Address recipients that don't publish a BIP 353 record and direct LNURL-pay links, while never shadowing a successful primary resolution. Enabling this opts in to revealing our IP and payment target to the LNURL endpoint, so it defaults to off. Co-developed with Claude Code (claude-opus-4-7).
1 parent c22b415 commit a21ecc1

4 files changed

Lines changed: 93 additions & 23 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ async-trait = { version = "0.1", default-features = false }
8181
vss-client = { package = "vss-client-ng", version = "0.5" }
8282
prost = { version = "0.11.6", default-features = false}
8383
#bitcoin-payment-instructions = { version = "0.6" }
84-
bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "679dac50cc0d81ec4d31da94b93d467e5308f16a" }
84+
bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "679dac50cc0d81ec4d31da94b93d467e5308f16a", features = ["http"] }
8585

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

src/builder.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use bitcoin::key::Secp256k1;
2121
use bitcoin::secp256k1::PublicKey;
2222
use bitcoin::Network;
2323
use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver;
24+
use bitcoin_payment_instructions::http_resolver::HTTPHrnResolver;
2425
use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver;
2526
use lightning::chain::{chainmonitor, BestBlock as BlockLocator};
2627
use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs};
@@ -80,8 +81,8 @@ use crate::runtime::{Runtime, RuntimeSpawner};
8081
use crate::tx_broadcaster::TransactionBroadcaster;
8182
use crate::types::{
8283
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
83-
GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore,
84-
PeerManager, PendingPaymentStore, SyncAndAsyncKVStore,
84+
GossipSync, Graph, HRNResolver, HRNResolverInner, KeysManager, MessageRouter, OnionMessenger,
85+
PaymentStore, PeerManager, PendingPaymentStore, SyncAndAsyncKVStore,
8586
};
8687
use crate::wallet::persist::KVStoreWalletPersister;
8788
use crate::wallet::Wallet;
@@ -1739,7 +1740,7 @@ fn build_with_store_internal(
17391740
})?;
17401741
}
17411742

1742-
let hrn_resolver;
1743+
let hrn_resolver_inner;
17431744
let mut blip32_resolver = None;
17441745

17451746
let runtime_handle = runtime.handle();
@@ -1751,7 +1752,7 @@ fn build_with_store_internal(
17511752
HRNResolverConfig::Blip32 => {
17521753
let hrn_res =
17531754
Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph)));
1754-
hrn_resolver = HRNResolver::Onion(Arc::clone(&hrn_res));
1755+
hrn_resolver_inner = HRNResolverInner::Onion(Arc::clone(&hrn_res));
17551756
blip32_resolver = Some(Arc::clone(&hrn_res));
17561757

17571758
hrn_res as Arc<dyn DNSResolverMessageHandler + Send + Sync>
@@ -1766,7 +1767,7 @@ fn build_with_store_internal(
17661767
BuildError::DNSResolverSetupFailed
17671768
})?;
17681769
let hrn_res = Arc::new(DNSHrnResolver(addr));
1769-
hrn_resolver = HRNResolver::Local(hrn_res);
1770+
hrn_resolver_inner = HRNResolverInner::Local(hrn_res);
17701771

17711772
if *enable_hrn_resolution_service {
17721773
if let Err(_) = may_announce_channel(&config) {
@@ -1790,6 +1791,13 @@ fn build_with_store_internal(
17901791
},
17911792
};
17921793

1794+
let http_hrn_resolver = if config.hrn_config.enable_lnurl_resolution {
1795+
Some(Arc::new(HTTPHrnResolver::new()))
1796+
} else {
1797+
None
1798+
};
1799+
let hrn_resolver = HRNResolver::new(hrn_resolver_inner, http_hrn_resolver);
1800+
17931801
// Initialize the PeerManager
17941802
let onion_messenger: Arc<OnionMessenger> =
17951803
if let Some(AsyncPaymentsRole::Server) = async_payments_role {

src/config.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,18 @@ pub struct HumanReadableNamesConfig {
266266
/// * **DNS Server**: `8.8.8.8:53` (Google Public DNS)
267267
/// * **Resolution Service**: Disabled (`false`)
268268
pub resolution_config: HRNResolverConfig,
269+
/// If set to true, enables resolving [LNURL-pay] links and the LN-Address LNURL fallback
270+
/// via HTTP, in addition to the BIP 353 / bLIP-32 resolution method configured in
271+
/// [`resolution_config`].
272+
///
273+
/// **Default:** `false`
274+
///
275+
/// **Note:** Enabling this may reveal our IP address to the recipient and information about
276+
/// who we're paying to the LNURL endpoint we're querying.
277+
///
278+
/// [LNURL-pay]: https://github.com/lnurl/luds/blob/luds/06.md
279+
/// [`resolution_config`]: Self::resolution_config
280+
pub enable_lnurl_resolution: bool,
269281
}
270282

271283
impl Default for HumanReadableNamesConfig {
@@ -276,6 +288,7 @@ impl Default for HumanReadableNamesConfig {
276288
.expect("Socket address conversion failed."),
277289
enable_hrn_resolution_service: false,
278290
},
291+
enable_lnurl_resolution: false,
279292
}
280293
}
281294
}

src/types.rs

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver;
1717
use bitcoin_payment_instructions::hrn_resolution::{
1818
HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture,
1919
};
20+
use bitcoin_payment_instructions::http_resolver::HTTPHrnResolver;
2021
use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver;
2122
use lightning::chain::chainmonitor;
2223
use lightning::impl_writeable_tlv_based;
@@ -329,37 +330,85 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse
329330
>;
330331

331332
#[derive(Clone)]
332-
pub enum HRNResolver {
333+
pub enum HRNResolverInner {
333334
Onion(Arc<LDKOnionMessageDNSSECHrnResolver<Arc<Graph>, Arc<Logger>>>),
334335
Local(Arc<DNSHrnResolver>),
335336
}
336337

338+
#[derive(Clone)]
339+
pub struct HRNResolver {
340+
inner: HRNResolverInner,
341+
http: Option<Arc<HTTPHrnResolver>>,
342+
}
343+
344+
impl HRNResolver {
345+
pub(crate) fn new(inner: HRNResolverInner, http: Option<Arc<HTTPHrnResolver>>) -> Self {
346+
Self { inner, http }
347+
}
348+
}
349+
337350
impl HrnResolver for HRNResolver {
338351
fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a> {
339-
match self {
340-
HRNResolver::Onion(inner) => inner.resolve_hrn(hrn),
341-
HRNResolver::Local(inner) => inner.resolve_hrn(hrn),
342-
}
352+
Box::pin(async move {
353+
let primary = match &self.inner {
354+
HRNResolverInner::Onion(inner) => inner.resolve_hrn(hrn).await,
355+
HRNResolverInner::Local(inner) => inner.resolve_hrn(hrn).await,
356+
};
357+
match (primary, &self.http) {
358+
(Ok(r), _) => Ok(r),
359+
(Err(_), Some(http)) => http.resolve_hrn(hrn).await,
360+
(Err(e), None) => Err(e),
361+
}
362+
})
343363
}
344364

345365
fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a> {
346-
match self {
347-
HRNResolver::Onion(inner) => inner.resolve_lnurl(url),
348-
HRNResolver::Local(inner) => inner.resolve_lnurl(url),
349-
}
366+
Box::pin(async move {
367+
let primary = match &self.inner {
368+
HRNResolverInner::Onion(inner) => inner.resolve_lnurl(url).await,
369+
HRNResolverInner::Local(inner) => inner.resolve_lnurl(url).await,
370+
};
371+
match (primary, &self.http) {
372+
(Ok(r), _) => Ok(r),
373+
(Err(_), Some(http)) => http.resolve_lnurl(url).await,
374+
(Err(e), None) => Err(e),
375+
}
376+
})
350377
}
351378

352379
fn resolve_lnurl_to_invoice<'a>(
353380
&'a self, callback_url: String, amount: BPIAmount, expected_description_hash: [u8; 32],
354381
) -> LNURLResolutionFuture<'a> {
355-
match self {
356-
HRNResolver::Onion(inner) => {
357-
inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash)
358-
},
359-
HRNResolver::Local(inner) => {
360-
inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash)
361-
},
362-
}
382+
Box::pin(async move {
383+
let primary = match &self.inner {
384+
HRNResolverInner::Onion(inner) => {
385+
inner
386+
.resolve_lnurl_to_invoice(
387+
callback_url.clone(),
388+
amount,
389+
expected_description_hash,
390+
)
391+
.await
392+
},
393+
HRNResolverInner::Local(inner) => {
394+
inner
395+
.resolve_lnurl_to_invoice(
396+
callback_url.clone(),
397+
amount,
398+
expected_description_hash,
399+
)
400+
.await
401+
},
402+
};
403+
match (primary, &self.http) {
404+
(Ok(r), _) => Ok(r),
405+
(Err(_), Some(http)) => {
406+
http.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash)
407+
.await
408+
},
409+
(Err(e), None) => Err(e),
410+
}
411+
})
363412
}
364413
}
365414

0 commit comments

Comments
 (0)