Skip to content

Commit 43bda7a

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 43bda7a

File tree

3 files changed

+233
-1
lines changed

3 files changed

+233
-1
lines changed

Cargo.toml

100644100755
+5
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

+1-1
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

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
let max_retries = 5;
98+
for attempt in 1..=max_retries {
99+
match lnd.query_routes(&node.node_id().to_string(), amount_msat).await {
100+
Ok(routes) => {
101+
if !routes.is_empty() {
102+
break;
103+
}
104+
},
105+
Err(err) => {
106+
if attempt == max_retries {
107+
panic!("Failed to find route from LND to LDK: {}", err);
108+
}
109+
},
110+
};
111+
// wait for the payment process
112+
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
113+
}
114+
115+
// Send a payment to LDK
116+
let invoice_description =
117+
Bolt11InvoiceDescription::Direct(Description::new("lndTest".to_string()).unwrap());
118+
let ldk_invoice =
119+
node.bolt11_payment().receive(amount_msat, &invoice_description, 3600).unwrap();
120+
lnd.pay_invoice(&ldk_invoice.to_string()).await;
121+
common::expect_event!(node, PaymentReceived);
122+
123+
node.close_channel(&user_channel_id, lnd_node_id).unwrap();
124+
common::expect_event!(node, ChannelClosed);
125+
node.stop().unwrap();
126+
}
127+
128+
struct TestLndClient {
129+
client: LndClient,
130+
}
131+
132+
impl TestLndClient {
133+
async fn new(cert_path: String, macaroon_path: String, socket: String) -> Self {
134+
// Read the contents of the file into a vector of bytes
135+
let cert_bytes = fs::read(cert_path).await.expect("Failed to read tls cert file");
136+
let mac_bytes = fs::read(macaroon_path).await.expect("Failed to read macaroon file");
137+
138+
// Convert the bytes to a hex string
139+
let cert = cert_bytes.as_hex().to_string();
140+
let macaroon = mac_bytes.as_hex().to_string();
141+
142+
let client = connect(cert, macaroon, socket).await.expect("Failed to connect to Lnd");
143+
144+
TestLndClient { client }
145+
}
146+
147+
async fn get_node_info(&mut self) -> LndGetInfoResponse {
148+
let response = self
149+
.client
150+
.lightning()
151+
.get_info(LndGetInfoRequest {})
152+
.await
153+
.expect("Failed to fetch node info from LND")
154+
.into_inner();
155+
156+
response
157+
}
158+
159+
async fn create_invoice(&mut self, amount_msat: u64) -> String {
160+
let invoice = LndInvoice { value_msat: amount_msat as i64, ..Default::default() };
161+
162+
self.client
163+
.lightning()
164+
.add_invoice(invoice)
165+
.await
166+
.expect("Failed to create invoice on LND")
167+
.into_inner()
168+
.payment_request
169+
}
170+
171+
async fn list_invoices(&mut self) -> Vec<LndInvoice> {
172+
self.client
173+
.lightning()
174+
.list_invoices(LndListInvoiceRequest { ..Default::default() })
175+
.await
176+
.expect("Failed to list invoices from LND")
177+
.into_inner()
178+
.invoices
179+
}
180+
181+
async fn query_routes(
182+
&mut self, pubkey: &str, amount_msat: u64,
183+
) -> Result<Vec<LndRoute>, String> {
184+
let request = LndQueryRoutesRequest {
185+
pub_key: pubkey.to_string(),
186+
amt_msat: amount_msat as i64,
187+
..Default::default()
188+
};
189+
190+
let response = self
191+
.client
192+
.lightning()
193+
.query_routes(request)
194+
.await
195+
.map_err(|err| format!("Failed to query routes from LND: {:?}", err))?
196+
.into_inner();
197+
198+
if response.routes.is_empty() {
199+
return Err(format!("No routes found for pubkey: {}", pubkey));
200+
}
201+
202+
Ok(response.routes)
203+
}
204+
205+
async fn pay_invoice(&mut self, invoice_str: &str) {
206+
let send_req =
207+
LndSendRequest { payment_request: invoice_str.to_string(), ..Default::default() };
208+
let response = self
209+
.client
210+
.lightning()
211+
.send_payment_sync(send_req)
212+
.await
213+
.expect("Failed to pay invoice on LND")
214+
.into_inner();
215+
216+
if !response.payment_error.is_empty() || response.payment_preimage.is_empty() {
217+
panic!(
218+
"LND payment failed: {}",
219+
if response.payment_error.is_empty() {
220+
"No preimage returned"
221+
} else {
222+
&response.payment_error
223+
}
224+
);
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)