Skip to content

Commit 626097c

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 #505
1 parent 9a38ab0 commit 626097c

File tree

3 files changed

+195
-1
lines changed

3 files changed

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

0 commit comments

Comments
 (0)