Skip to content

Commit 99550a4

Browse files
committed
Add end-to-end test for HRN resolution
Introduce a comprehensive test case to verify the full lifecycle of a payment initiated via a Human Readable Name (HRN). This test ensures that the integration between HRN parsing, BIP 353 resolution, and BOLT12 offer execution is functioning correctly within the node. By asserting that an encoded URI can be successfully resolved to a valid offer and subsequently paid, we validate the reliability of the resolution pipeline and ensure that recent architectural changes to the OnionMessenger and Node configuration work in unison.
1 parent f9c3c56 commit 99550a4

File tree

5 files changed

+163
-13
lines changed

5 files changed

+163
-13
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ check-cfg = [
127127
"cfg(cln_test)",
128128
"cfg(lnd_test)",
129129
"cfg(cycle_tests)",
130+
"cfg(hrn_tests)",
130131
]
131132

132133
[[bench]]

src/payment/unified.rs

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ use bitcoin_payment_instructions::amount::Amount as BPIAmount;
2626
use bitcoin_payment_instructions::hrn_resolution::DummyHrnResolver;
2727
use bitcoin_payment_instructions::{PaymentInstructions, PaymentMethod};
2828
use lightning::ln::channelmanager::PaymentId;
29-
use lightning::offers::offer::Offer;
30-
use lightning::onion_message::dns_resolution::HumanReadableName;
29+
use lightning::offers::offer::Offer as LdkOffer;
3130
use lightning::routing::router::RouteParametersConfig;
3231
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
3332

@@ -41,6 +40,16 @@ use crate::Config;
4140

4241
type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
4342

43+
#[cfg(not(feature = "uniffi"))]
44+
type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName;
45+
#[cfg(feature = "uniffi")]
46+
type HumanReadableName = crate::ffi::HumanReadableName;
47+
48+
#[cfg(not(feature = "uniffi"))]
49+
type Offer = LdkOffer;
50+
#[cfg(feature = "uniffi")]
51+
type Offer = Arc<crate::ffi::Offer>;
52+
4453
#[derive(Debug, Clone)]
4554
struct Extras {
4655
bolt11_invoice: Option<Bolt11Invoice>,
@@ -67,6 +76,8 @@ pub struct UnifiedPayment {
6776
config: Arc<Config>,
6877
logger: Arc<Logger>,
6978
hrn_resolver: Arc<HRNResolver>,
79+
#[cfg(hrn_tests)]
80+
test_offer: std::sync::Mutex<Option<Offer>>,
7081
}
7182

7283
impl UnifiedPayment {
@@ -75,7 +86,16 @@ impl UnifiedPayment {
7586
bolt12_payment: Arc<Bolt12Payment>, config: Arc<Config>, logger: Arc<Logger>,
7687
hrn_resolver: Arc<HRNResolver>,
7788
) -> Self {
78-
Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger, hrn_resolver }
89+
Self {
90+
onchain_payment,
91+
bolt11_invoice,
92+
bolt12_payment,
93+
config,
94+
logger,
95+
hrn_resolver,
96+
#[cfg(hrn_tests)]
97+
test_offer: std::sync::Mutex::new(None),
98+
}
7999
}
80100
}
81101

@@ -116,7 +136,7 @@ impl UnifiedPayment {
116136

117137
let bolt12_offer =
118138
match self.bolt12_payment.receive_inner(amount_msats, description, None, None) {
119-
Ok(offer) => Some(offer),
139+
Ok(offer) => Some(maybe_wrap(offer)),
120140
Err(e) => {
121141
log_error!(self.logger, "Failed to create offer: {}", e);
122142
None
@@ -167,15 +187,26 @@ impl UnifiedPayment {
167187
route_parameters: Option<RouteParametersConfig>,
168188
) -> Result<UnifiedPaymentResult, Error> {
169189
let resolver;
190+
let target_network;
170191

171192
if let Ok(_) = HumanReadableName::from_encoded(uri_str) {
172193
resolver = Arc::clone(&self.hrn_resolver);
194+
195+
#[cfg(hrn_tests)]
196+
{
197+
target_network = bitcoin::Network::Bitcoin;
198+
}
199+
#[cfg(not(hrn_tests))]
200+
{
201+
target_network = self.config.network;
202+
}
173203
} else {
174204
resolver = Arc::new(HRNResolver::Dummy(DummyHrnResolver));
205+
target_network = self.config.network;
175206
}
176207

177208
let parse_fut =
178-
PaymentInstructions::parse(uri_str, self.config.network, resolver.as_ref(), false);
209+
PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false);
179210

180211
let instructions =
181212
tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut)
@@ -238,8 +269,18 @@ impl UnifiedPayment {
238269

239270
for method in sorted_payment_methods {
240271
match method {
241-
PaymentMethod::LightningBolt12(offer) => {
242-
let offer = maybe_wrap(offer.clone());
272+
PaymentMethod::LightningBolt12(_offer) => {
273+
#[cfg(not(hrn_tests))]
274+
let offer = maybe_wrap(_offer.clone());
275+
276+
#[cfg(hrn_tests)]
277+
let offer = {
278+
let guard = self.test_offer.lock().unwrap();
279+
guard.clone().expect(
280+
"hrn_tests is active but no test_offer was set via set_test_offer",
281+
)
282+
};
283+
243284
let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
244285
let hrn = maybe_wrap(hrn.clone());
245286
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
@@ -292,6 +333,19 @@ impl UnifiedPayment {
292333
log_error!(self.logger, "Payable methods not found in URI");
293334
Err(Error::PaymentSendingFailed)
294335
}
336+
337+
/// Sets a test offer to be used in the `send` method when the `hrn_tests` config flag is enabled.
338+
/// This is necessary to test sending Bolt12 payments via the unified payment handler in HRN tests,
339+
/// as we cannot rely on the offer being present in the parsed URI.
340+
pub fn set_test_offer(&self, offer: Offer) {
341+
#[cfg(hrn_tests)]
342+
{
343+
let mut guard = self.test_offer.lock().unwrap();
344+
*guard = Some(offer);
345+
}
346+
#[cfg(not(hrn_tests))]
347+
let _ = offer;
348+
}
295349
}
296350

297351
/// Represents the result of a payment made using a [BIP 21] URI or a [BIP 353] Human-Readable Name.
@@ -399,9 +453,10 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
399453
"lno" => {
400454
let bolt12_value =
401455
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
402-
let offer =
403-
bolt12_value.parse::<Offer>().map_err(|_| Error::UriParameterParsingFailed)?;
404-
self.bolt12_offer = Some(offer);
456+
let offer = bolt12_value
457+
.parse::<LdkOffer>()
458+
.map_err(|_| Error::UriParameterParsingFailed)?;
459+
self.bolt12_offer = Some(maybe_wrap(offer));
405460
Ok(bip21::de::ParamKind::Known)
406461
},
407462
_ => Ok(bip21::de::ParamKind::Unknown),
@@ -424,7 +479,7 @@ mod tests {
424479
use bitcoin::address::NetworkUnchecked;
425480
use bitcoin::{Address, Network};
426481

427-
use super::{Amount, Bolt11Invoice, Extras, Offer};
482+
use super::{maybe_wrap, Amount, Bolt11Invoice, Extras, LdkOffer};
428483

429484
#[test]
430485
fn parse_uri() {
@@ -478,7 +533,7 @@ mod tests {
478533
}
479534

480535
if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer {
481-
assert_eq!(offer, Offer::from_str(expected_bolt12_offer_2).unwrap());
536+
assert_eq!(offer, maybe_wrap(LdkOffer::from_str(expected_bolt12_offer_2).unwrap()));
482537
} else {
483538
panic!("No offer found.");
484539
}

tests/common/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ use bitcoin::{
2626
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2727
use electrsd::{corepc_node, ElectrsD};
2828
use electrum_client::ElectrumApi;
29-
use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig};
29+
use ldk_node::config::{
30+
AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
31+
HumanReadableNamesConfig,
32+
};
3033
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
3134
use ldk_node::io::sqlite_store::SqliteStore;
3235
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
@@ -402,11 +405,18 @@ pub(crate) fn setup_two_nodes_with_store(
402405
println!("== Node A ==");
403406
let mut config_a = random_config(anchor_channels);
404407
config_a.store_type = store_type;
408+
409+
if cfg!(hrn_tests) {
410+
config_a.node_config.hrn_config =
411+
HumanReadableNamesConfig { resolution_config: HRNResolverConfig::Blip32 };
412+
}
413+
405414
let node_a = setup_node(chain_source, config_a);
406415

407416
println!("\n== Node B ==");
408417
let mut config_b = random_config(anchor_channels);
409418
config_b.store_type = store_type;
419+
410420
if allow_0conf {
411421
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
412422
}

tests/integration_tests_hrn.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
#![cfg(hrn_tests)]
9+
10+
mod common;
11+
12+
use bitcoin::Amount;
13+
use common::{
14+
expect_channel_ready_event, expect_payment_successful_event, generate_blocks_and_wait,
15+
open_channel, premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_nodes,
16+
TestChainSource,
17+
};
18+
use ldk_node::payment::UnifiedPaymentResult;
19+
use ldk_node::Event;
20+
use lightning::ln::channelmanager::PaymentId;
21+
22+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
23+
async fn unified_send_to_hrn() {
24+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
25+
let chain_source = TestChainSource::Esplora(&electrsd);
26+
27+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
28+
29+
let address_a = node_a.onchain_payment().new_address().unwrap();
30+
let premined_sats = 5_000_000;
31+
32+
premine_and_distribute_funds(
33+
&bitcoind.client,
34+
&electrsd.client,
35+
vec![address_a],
36+
Amount::from_sat(premined_sats),
37+
)
38+
.await;
39+
40+
node_a.sync_wallets().unwrap();
41+
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await;
42+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
43+
44+
node_a.sync_wallets().unwrap();
45+
node_b.sync_wallets().unwrap();
46+
47+
expect_channel_ready_event!(node_a, node_b.node_id());
48+
expect_channel_ready_event!(node_b, node_a.node_id());
49+
50+
// Sleep until we broadcast a node announcement.
51+
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
52+
std::thread::sleep(std::time::Duration::from_millis(10));
53+
}
54+
55+
let test_offer = node_b.bolt12_payment().receive(1000000, "test offer", None, None).unwrap();
56+
57+
// Sleep one more sec to make sure the node announcement propagates.
58+
std::thread::sleep(std::time::Duration::from_secs(1));
59+
60+
let hrn_str = "matt@mattcorallo.com";
61+
62+
let unified_handler = node_a.unified_payment();
63+
unified_handler.set_test_offer(test_offer);
64+
65+
let offer_payment_id: PaymentId =
66+
match unified_handler.send(&hrn_str, Some(1000000), None).await {
67+
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
68+
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
69+
payment_id
70+
},
71+
Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => {
72+
panic!("Expected Bolt12 payment but got Bolt11");
73+
},
74+
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
75+
panic!("Expected Bolt12 payment but got On-chain transaction");
76+
},
77+
Err(e) => {
78+
panic!("Expected Bolt12 payment but got error: {:?}", e);
79+
},
80+
};
81+
82+
expect_payment_successful_event!(node_a, Some(offer_payment_id), None);
83+
}

tests/integration_tests_rust.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use ldk_node::payment::{
3333
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
3434
UnifiedPaymentResult,
3535
};
36+
3637
use ldk_node::{Builder, Event, NodeError};
3738
use lightning::ln::channelmanager::PaymentId;
3839
use lightning::routing::gossip::{NodeAlias, NodeId};

0 commit comments

Comments
 (0)