Skip to content

Commit 0778cb1

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 562a608 commit 0778cb1

File tree

4 files changed

+158
-13
lines changed

4 files changed

+158
-13
lines changed

Cargo.toml

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

133134
[[bench]]

src/payment/unified.rs

Lines changed: 63 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,15 @@ 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+
#[cfg(hrn_tests)]
341+
pub fn set_test_offer(&self, offer: Offer) {
342+
let mut guard = self.test_offer.lock().unwrap();
343+
*guard = Some(offer);
344+
}
295345
}
296346

297347
/// Represents the result of a payment made using a [BIP 21] URI or a [BIP 353] Human-Readable Name.
@@ -399,9 +449,10 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
399449
"lno" => {
400450
let bolt12_value =
401451
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);
452+
let offer = bolt12_value
453+
.parse::<LdkOffer>()
454+
.map_err(|_| Error::UriParameterParsingFailed)?;
455+
self.bolt12_offer = Some(maybe_wrap(offer));
405456
Ok(bip21::de::ParamKind::Known)
406457
},
407458
_ => Ok(bip21::de::ParamKind::Unknown),
@@ -424,7 +475,7 @@ mod tests {
424475
use bitcoin::address::NetworkUnchecked;
425476
use bitcoin::{Address, Network};
426477

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

429480
#[test]
430481
fn parse_uri() {
@@ -478,7 +529,7 @@ mod tests {
478529
}
479530

480531
if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer {
481-
assert_eq!(offer, Offer::from_str(expected_bolt12_offer_2).unwrap());
532+
assert_eq!(offer, maybe_wrap(LdkOffer::from_str(expected_bolt12_offer_2).unwrap()));
482533
} else {
483534
panic!("No offer found.");
484535
}

tests/common/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ use bitcoin::{
2727
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2828
use electrsd::{corepc_node, ElectrsD};
2929
use electrum_client::ElectrumApi;
30-
use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig};
30+
use ldk_node::config::{
31+
AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
32+
HumanReadableNamesConfig,
33+
};
3134
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
3235
use ldk_node::io::sqlite_store::SqliteStore;
3336
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
@@ -400,11 +403,18 @@ pub(crate) fn setup_two_nodes_with_store(
400403
println!("== Node A ==");
401404
let mut config_a = random_config(anchor_channels);
402405
config_a.store_type = store_type;
406+
407+
if cfg!(hrn_tests) {
408+
config_a.node_config.hrn_config =
409+
HumanReadableNamesConfig { resolution_config: HRNResolverConfig::Blip32 };
410+
}
411+
403412
let node_a = setup_node(chain_source, config_a);
404413

405414
println!("\n== Node B ==");
406415
let mut config_b = random_config(anchor_channels);
407416
config_b.store_type = store_type;
417+
408418
if allow_0conf {
409419
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
410420
}

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, random_chain_source, setup_bitcoind_and_electrsd,
16+
setup_two_nodes, 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 = random_chain_source(&bitcoind, &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+
}

0 commit comments

Comments
 (0)