Skip to content

Commit a56230f

Browse files
feat: implement LND integration tests with lnd_grpc_rust
Adds tests to: - Open a payment channel with LND. - Request and pay an invoice, verifying receipt. - Generate an invoice for LND to pay, verifying receipt. Uses lnd_grpc_rust for gRPC communication. Closes lightningdevkit#505
1 parent 9a38ab0 commit a56230f

File tree

3 files changed

+231
-1
lines changed

3 files changed

+231
-1
lines changed

Cargo.toml

100644100755
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ electrsd = { version = "0.29.0", features = ["legacy"] }
106106
[target.'cfg(cln_test)'.dev-dependencies]
107107
clightningrpc = { version = "0.3.0-beta.8", default-features = false }
108108

109+
[target.'cfg(lnd_test)'.dev-dependencies]
110+
lnd_grpc_rust = { version = "2.10.0", default-features = false }
111+
tokio = { version = "1.37", features = ["fs"] }
112+
109113
[build-dependencies]
110114
uniffi = { version = "0.27.3", features = ["build"], optional = true }
111115

@@ -123,4 +127,5 @@ check-cfg = [
123127
"cfg(ldk_bench)",
124128
"cfg(tokio_unstable)",
125129
"cfg(cln_test)",
130+
"cfg(lnd_test)",
126131
]

tests/common/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8-
#![cfg(any(test, cln_test, vss_test))]
8+
#![cfg(any(test, cln_test, lnd_test, vss_test))]
99
#![allow(dead_code)]
1010

1111
pub(crate) mod logging;

tests/integration_tests_lnd.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// #![cfg(lnd_test)]
2+
3+
mod common;
4+
5+
use ldk_node::bitcoin::secp256k1::PublicKey;
6+
use ldk_node::bitcoin::Amount;
7+
use ldk_node::lightning::ln::msgs::SocketAddress;
8+
use ldk_node::{Builder, Event};
9+
10+
use lnd_grpc_rust::lnrpc::{
11+
invoice::InvoiceState::Settled as LndInvoiceStateSettled, GetInfoRequest as LndGetInfoRequest,
12+
GetInfoResponse as LndGetInfoResponse, Invoice as LndInvoice,
13+
ListInvoiceRequest as LndListInvoiceRequest, QueryRoutesRequest as LndQueryRoutesRequest,
14+
Route as LndRoute, SendRequest as LndSendRequest,
15+
};
16+
use lnd_grpc_rust::{connect, LndClient};
17+
18+
use bitcoincore_rpc::Auth;
19+
use bitcoincore_rpc::Client as BitcoindClient;
20+
21+
use electrum_client::Client as ElectrumClient;
22+
use lightning_invoice::{Bolt11InvoiceDescription, Description};
23+
24+
use bitcoin::hex::DisplayHex;
25+
26+
use std::default::Default;
27+
use std::str::FromStr;
28+
use tokio::fs;
29+
30+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
31+
async fn test_lnd() {
32+
// Setup bitcoind / electrs clients
33+
let bitcoind_client = BitcoindClient::new(
34+
"127.0.0.1:18443",
35+
Auth::UserPass("user".to_string(), "pass".to_string()),
36+
)
37+
.unwrap();
38+
let electrs_client = ElectrumClient::new("tcp://127.0.0.1:50001").unwrap();
39+
40+
// Give electrs a kick.
41+
common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1);
42+
43+
// Setup LDK Node
44+
let config = common::random_config(true);
45+
let mut builder = Builder::from_config(config.node_config);
46+
builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None);
47+
48+
let node = builder.build().unwrap();
49+
node.start().unwrap();
50+
51+
// Premine some funds and distribute
52+
let address = node.onchain_payment().new_address().unwrap();
53+
let premine_amount = Amount::from_sat(5_000_000);
54+
common::premine_and_distribute_funds(
55+
&bitcoind_client,
56+
&electrs_client,
57+
vec![address],
58+
premine_amount,
59+
);
60+
61+
// Setup LND
62+
let endpoint = "127.0.0.1:8081";
63+
let cert_path = std::env::var("LND_CERT_PATH").expect("LND_CERT_PATH not set");
64+
let macaroon_path = std::env::var("LND_MACAROON_PATH").expect("LND_MACAROON_PATH not set");
65+
let mut lnd = TestLndClient::new(cert_path, macaroon_path, endpoint.to_string()).await;
66+
67+
let lnd_node_info = lnd.get_node_info().await;
68+
let lnd_node_id = PublicKey::from_str(&lnd_node_info.identity_pubkey).unwrap();
69+
let lnd_address: SocketAddress = "127.0.0.1:9735".parse().unwrap();
70+
71+
node.sync_wallets().unwrap();
72+
73+
// Open the channel
74+
let funding_amount_sat = 1_000_000;
75+
76+
node.open_channel(lnd_node_id, lnd_address, funding_amount_sat, Some(500_000_000), None)
77+
.unwrap();
78+
79+
let funding_txo = common::expect_channel_pending_event!(node, lnd_node_id);
80+
common::wait_for_tx(&electrs_client, funding_txo.txid);
81+
common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 6);
82+
node.sync_wallets().unwrap();
83+
let user_channel_id = common::expect_channel_ready_event!(node, lnd_node_id);
84+
85+
// Send a payment to LND
86+
let lnd_invoice = lnd.create_invoice(100_000_000).await;
87+
let parsed_invoice = lightning_invoice::Bolt11Invoice::from_str(&lnd_invoice).unwrap();
88+
89+
node.bolt11_payment().send(&parsed_invoice, None).unwrap();
90+
common::expect_event!(node, PaymentSuccessful);
91+
let lnd_listed_invoices = lnd.list_invoices().await;
92+
assert_eq!(lnd_listed_invoices.len(), 1);
93+
assert_eq!(lnd_listed_invoices.first().unwrap().state, LndInvoiceStateSettled as i32);
94+
95+
// Check route LND -> LDK
96+
let amount_msat = 9_000_000;
97+
98+
let max_retries = 5;
99+
for attempt in 1..=max_retries {
100+
match lnd.query_routes(&node.node_id().to_string(), amount_msat).await {
101+
Ok(routes) => {
102+
if !routes.is_empty() {
103+
break;
104+
}
105+
},
106+
Err(err) => {
107+
if attempt == max_retries {
108+
panic!("Failed to find route from LND to LDK: {}", err);
109+
}
110+
},
111+
};
112+
}
113+
114+
// Send a payment to LDK
115+
let invoice_description =
116+
Bolt11InvoiceDescription::Direct(Description::new("lndTest".to_string()).unwrap());
117+
let ldk_invoice = node.bolt11_payment().receive(9_000_000, &invoice_description, 3600).unwrap();
118+
lnd.pay_invoice(&ldk_invoice.to_string()).await;
119+
common::expect_event!(node, PaymentReceived);
120+
121+
node.close_channel(&user_channel_id, lnd_node_id).unwrap();
122+
common::expect_event!(node, ChannelClosed);
123+
node.stop().unwrap();
124+
}
125+
126+
struct TestLndClient {
127+
client: LndClient,
128+
}
129+
130+
impl TestLndClient {
131+
async fn new(cert_path: String, macaroon_path: String, socket: String) -> Self {
132+
// Read the contents of the file into a vector of bytes
133+
let cert_bytes = fs::read(cert_path).await.expect("Failed to read tls cert file");
134+
let mac_bytes = fs::read(macaroon_path).await.expect("Failed to read macaroon file");
135+
136+
// Convert the bytes to a hex string
137+
let cert = cert_bytes.as_hex().to_string();
138+
let macaroon = mac_bytes.as_hex().to_string();
139+
140+
let client = connect(cert, macaroon, socket).await.expect("Failed to connect to Lnd");
141+
142+
TestLndClient { client }
143+
}
144+
145+
async fn get_node_info(&mut self) -> LndGetInfoResponse {
146+
let response = self
147+
.client
148+
.lightning()
149+
.get_info(LndGetInfoRequest {})
150+
.await
151+
.expect("Failed to fetch node info from LND")
152+
.into_inner();
153+
154+
response
155+
}
156+
157+
async fn create_invoice(&mut self, amount_msat: u64) -> String {
158+
let invoice = LndInvoice { value_msat: amount_msat as i64, ..Default::default() };
159+
160+
self.client
161+
.lightning()
162+
.add_invoice(invoice)
163+
.await
164+
.expect("Failed to create invoice on LND")
165+
.into_inner()
166+
.payment_request
167+
}
168+
169+
async fn list_invoices(&mut self) -> Vec<LndInvoice> {
170+
self.client
171+
.lightning()
172+
.list_invoices(LndListInvoiceRequest { ..Default::default() })
173+
.await
174+
.expect("Failed to list invoices from LND")
175+
.into_inner()
176+
.invoices
177+
}
178+
179+
async fn query_routes(
180+
&mut self, pubkey: &str, amount_msat: u64,
181+
) -> Result<Vec<LndRoute>, String> {
182+
let request = LndQueryRoutesRequest {
183+
pub_key: pubkey.to_string(),
184+
amt_msat: amount_msat as i64,
185+
..Default::default()
186+
};
187+
188+
let response = self
189+
.client
190+
.lightning()
191+
.query_routes(request)
192+
.await
193+
.map_err(|err| format!("Failed to query routes from LND: {:?}", err))?
194+
.into_inner();
195+
196+
if response.routes.is_empty() {
197+
return Err(format!("No routes found for pubkey: {}", pubkey));
198+
}
199+
200+
Ok(response.routes)
201+
}
202+
203+
async fn pay_invoice(&mut self, invoice_str: &str) {
204+
let send_req =
205+
LndSendRequest { payment_request: invoice_str.to_string(), ..Default::default() };
206+
let response = self
207+
.client
208+
.lightning()
209+
.send_payment_sync(send_req)
210+
.await
211+
.expect("Failed to pay invoice on LND")
212+
.into_inner();
213+
214+
if !response.payment_error.is_empty() || response.payment_preimage.is_empty() {
215+
panic!(
216+
"LND payment failed: {}",
217+
if response.payment_error.is_empty() {
218+
"No preimage returned"
219+
} else {
220+
&response.payment_error
221+
}
222+
);
223+
}
224+
}
225+
}

0 commit comments

Comments
 (0)