Skip to content

Commit c746a64

Browse files
committed
test: add BIP-353 integration test for pay_offer with name resolution
Add integration test validating end-to-end payment using human-readable names. Uses mock DNS resolver to test server-side name resolution and payment flow.
1 parent 1a56d82 commit c746a64

2 files changed

Lines changed: 243 additions & 2 deletions

File tree

src/dns_resolver.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ use bitcoin_payment_instructions::hrn_resolution::{HrnResolution, HrnResolver, H
33
use bitcoin_payment_instructions::http_resolver::HTTPHrnResolver;
44
use std::sync::Arc;
55

6+
#[cfg(itest)]
7+
use std::sync::RwLock;
8+
9+
#[cfg(itest)]
10+
pub static TEST_RESOLVER: RwLock<Option<Arc<dyn HrnResolver + Send + Sync>>> = RwLock::new(None);
11+
612
pub struct LndkDNSResolverMessageHandler {
713
resolver: Arc<dyn HrnResolver + Send + Sync>,
814
}
@@ -15,6 +21,16 @@ impl Default for LndkDNSResolverMessageHandler {
1521

1622
impl LndkDNSResolverMessageHandler {
1723
pub fn new() -> Self {
24+
#[cfg(itest)]
25+
{
26+
if let Ok(guard) = TEST_RESOLVER.read() {
27+
if let Some(test_resolver) = guard.as_ref() {
28+
return Self {
29+
resolver: Arc::clone(test_resolver),
30+
};
31+
}
32+
}
33+
}
1834
Self::with_resolver(HTTPHrnResolver::new())
1935
}
2036

tests/integration_tests.rs

Lines changed: 227 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ use std::net::SocketAddr;
2727
use std::path::PathBuf;
2828
use std::str::FromStr;
2929
use std::sync::Arc;
30-
use std::time::SystemTime;
30+
use std::time::{Duration, SystemTime};
3131
use tokio::time;
32-
use tokio::time::Duration;
32+
3333
use tokio::{select, try_join};
3434
use tonic_lnd::Client;
3535

@@ -1440,3 +1440,228 @@ async fn test_receive_payment_from_offer_with_multiple_blinded_paths() {
14401440
}
14411441
};
14421442
}
1443+
1444+
struct MockHrnResolver {
1445+
response_uri: String,
1446+
}
1447+
1448+
fn set_test_dns_resolver<
1449+
R: bitcoin_payment_instructions::hrn_resolution::HrnResolver + Send + Sync + 'static,
1450+
>(
1451+
resolver: R,
1452+
) {
1453+
if let Ok(mut guard) = lndk::dns_resolver::TEST_RESOLVER.write() {
1454+
*guard = Some(std::sync::Arc::new(resolver));
1455+
}
1456+
}
1457+
1458+
fn clear_test_dns_resolver() {
1459+
if let Ok(mut guard) = lndk::dns_resolver::TEST_RESOLVER.write() {
1460+
*guard = None;
1461+
}
1462+
}
1463+
1464+
impl bitcoin_payment_instructions::hrn_resolution::HrnResolver for MockHrnResolver {
1465+
fn resolve_hrn<'a>(
1466+
&'a self,
1467+
_hrn: &'a bitcoin_payment_instructions::hrn_resolution::HumanReadableName,
1468+
) -> bitcoin_payment_instructions::hrn_resolution::HrnResolutionFuture<'a> {
1469+
let uri = self.response_uri.clone();
1470+
Box::pin(async move {
1471+
Ok(
1472+
bitcoin_payment_instructions::hrn_resolution::HrnResolution::DNSSEC {
1473+
proof: None,
1474+
result: uri,
1475+
},
1476+
)
1477+
})
1478+
}
1479+
1480+
fn resolve_lnurl<'a>(
1481+
&'a self,
1482+
_url: &'a str,
1483+
) -> bitcoin_payment_instructions::hrn_resolution::HrnResolutionFuture<'a> {
1484+
Box::pin(async { Err("LNURL resolution not supported in mock") })
1485+
}
1486+
1487+
fn resolve_lnurl_to_invoice<'a>(
1488+
&'a self,
1489+
_callback_url: String,
1490+
_amount: bitcoin_payment_instructions::amount::Amount,
1491+
_expected_description_hash: [u8; 32],
1492+
) -> bitcoin_payment_instructions::hrn_resolution::LNURLResolutionFuture<'a> {
1493+
Box::pin(async { Err("LNURL invoice resolution not supported in mock") })
1494+
}
1495+
}
1496+
1497+
#[tokio::test(flavor = "multi_thread")]
1498+
async fn test_pay_offer_with_name_and_dns() {
1499+
use lndk::lndkrpc::offers_client::OffersClient;
1500+
use lndk::lndkrpc::offers_server::OffersServer;
1501+
use lndk::lndkrpc::PayOfferRequest;
1502+
use std::time::{Duration, SystemTime};
1503+
use tonic::transport::{Identity, Server, ServerTlsConfig};
1504+
1505+
let test_name = "lndk_pay_offer_with_name";
1506+
let (bitcoind, mut lnd, ldk1, ldk2, lndk_dir, _) =
1507+
common::setup_test_infrastructure(test_name).await;
1508+
1509+
let (ldk1_pubkey, ldk2_pubkey, _) =
1510+
common::connect_network(&ldk1, &ldk2, false, true, &mut lnd, &bitcoind).await;
1511+
1512+
let path_pubkeys = vec![ldk2_pubkey, ldk1_pubkey];
1513+
let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
1514+
let offer = ldk1
1515+
.create_offer(
1516+
&path_pubkeys,
1517+
Network::Regtest,
1518+
20_000,
1519+
Quantity::One,
1520+
expiration,
1521+
)
1522+
.await
1523+
.expect("should create offer");
1524+
1525+
let offer_string = offer.to_string();
1526+
let human_readable_name = "satoshi@bip353.test";
1527+
1528+
let mock_resolver = MockHrnResolver {
1529+
response_uri: format!("bitcoin:?lno={}", offer_string),
1530+
};
1531+
1532+
set_test_dns_resolver(mock_resolver);
1533+
1534+
let dns_resolver = lndk::dns_resolver::LndkDNSResolverMessageHandler::new();
1535+
let resolved_offer = dns_resolver
1536+
.resolve_name_to_offer(human_readable_name)
1537+
.await;
1538+
assert!(
1539+
resolved_offer.is_ok(),
1540+
"DNS resolution failed: {:?}",
1541+
resolved_offer.err()
1542+
);
1543+
assert_eq!(resolved_offer.unwrap(), offer_string);
1544+
1545+
let (lndk_cfg, handler, messenger, shutdown) = common::setup_lndk(
1546+
&lnd.cert_path,
1547+
&lnd.macaroon_path,
1548+
lnd.address.clone(),
1549+
lndk_dir.clone(),
1550+
)
1551+
.await;
1552+
1553+
let mut lnd_client = lnd.client.clone().unwrap();
1554+
let info = lnd_client
1555+
.lightning()
1556+
.get_info(tonic_lnd::lnrpc::GetInfoRequest {})
1557+
.await
1558+
.expect("failed to get info")
1559+
.into_inner();
1560+
1561+
let server_addr = format!(
1562+
"{}:{}",
1563+
lndk::DEFAULT_SERVER_HOST,
1564+
lndk::DEFAULT_SERVER_PORT
1565+
);
1566+
let server_socket_addr: SocketAddr = server_addr.parse().unwrap();
1567+
1568+
let lnd_cert_path = PathBuf::from(&lnd.cert_path);
1569+
let lnd_cert = std::fs::read_to_string(lnd_cert_path).unwrap();
1570+
1571+
let lndk_server = lndk::server::LNDKServer::new(
1572+
handler.clone(),
1573+
&info.identity_pubkey,
1574+
lnd_cert.clone(),
1575+
lnd.address.clone(),
1576+
)
1577+
.await;
1578+
1579+
let tls_cert_path = lndk_dir.join(lndk::TLS_CERT_FILENAME);
1580+
let tls_key_path = lndk_dir.join(lndk::TLS_KEY_FILENAME);
1581+
1582+
if !tls_cert_path.exists() || !tls_key_path.exists() {
1583+
use rcgen::generate_simple_self_signed;
1584+
let cert_key = generate_simple_self_signed(vec!["localhost".to_string()])
1585+
.expect("failed to generate certificate");
1586+
std::fs::write(
1587+
&tls_key_path,
1588+
cert_key.signing_key.serialize_pem().as_bytes(),
1589+
)
1590+
.expect("failed to write key");
1591+
std::fs::write(&tls_cert_path, cert_key.cert.pem()).expect("failed to write cert");
1592+
}
1593+
1594+
let tls_cert = std::fs::read_to_string(tls_cert_path.clone()).unwrap();
1595+
let tls_key = std::fs::read_to_string(tls_key_path).unwrap();
1596+
let identity = Identity::from_pem(tls_cert.clone(), tls_key);
1597+
1598+
let (server_shutdown, server_listener) = triggered::trigger();
1599+
let server_handle = tokio::spawn(async move {
1600+
Server::builder()
1601+
.tls_config(ServerTlsConfig::new().identity(identity))
1602+
.expect("couldn't configure tls")
1603+
.add_service(OffersServer::new(lndk_server))
1604+
.serve_with_shutdown(server_socket_addr, server_listener)
1605+
.await
1606+
});
1607+
1608+
tokio::time::sleep(Duration::from_secs(1)).await;
1609+
1610+
select! {
1611+
val = messenger.run(lndk_cfg, Arc::clone(&handler)) => {
1612+
panic!("messenger should not complete first {:?}", val);
1613+
},
1614+
result = async {
1615+
let lndk_cert_pem = std::fs::read_to_string(tls_cert_path).unwrap();
1616+
let macaroon_bytes = std::fs::read(&lnd.macaroon_path).unwrap();
1617+
let macaroon_hex = hex::encode(macaroon_bytes);
1618+
1619+
let cert = tonic::transport::Certificate::from_pem(lndk_cert_pem.as_bytes());
1620+
let tls_config = tonic::transport::ClientTlsConfig::new()
1621+
.ca_certificate(cert)
1622+
.domain_name("localhost");
1623+
1624+
let channel = tonic::transport::Channel::from_shared(format!("https://{}", server_addr))
1625+
.unwrap()
1626+
.tls_config(tls_config)
1627+
.unwrap()
1628+
.connect()
1629+
.await
1630+
.expect("failed to connect to lndk server");
1631+
1632+
let mut client = OffersClient::new(channel);
1633+
1634+
let mut request = tonic::Request::new(PayOfferRequest {
1635+
offer: String::new(),
1636+
amount: Some(20_000),
1637+
payer_note: None,
1638+
response_invoice_timeout: None,
1639+
fee_limit: None,
1640+
fee_limit_percent: None,
1641+
name: Some(human_readable_name.to_string()),
1642+
});
1643+
1644+
request.metadata_mut().insert(
1645+
"macaroon",
1646+
macaroon_hex.parse().expect("failed to parse macaroon"),
1647+
);
1648+
1649+
let result = client.pay_offer(request).await;
1650+
assert!(result.is_ok(), "pay_offer failed: {:?}", result.err());
1651+
1652+
let response = result.as_ref().unwrap().get_ref();
1653+
assert!(!response.payment_preimage.is_empty(), "Payment should have a preimage");
1654+
result
1655+
} => {
1656+
assert!(result.is_ok());
1657+
1658+
server_shutdown.trigger();
1659+
shutdown.trigger();
1660+
let _ = tokio::time::timeout(Duration::from_secs(5), server_handle).await;
1661+
ldk1.stop().await;
1662+
ldk2.stop().await;
1663+
1664+
clear_test_dns_resolver();
1665+
}
1666+
}
1667+
}

0 commit comments

Comments
 (0)