1use anyhow::Context;
2use secp256k1::PublicKey;
3use std::env;
4use std::fs;
5use std::net::SocketAddr;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8
9const CREDENTIALS_PATH_ENV: &str = "LNURL_BALANCER_CREDENTIALS_PATH";
10const SKIP_INTEGRATION_TESTS_ENV: &str = "LNURL_SKIP_INTEGRATION_TESTS";
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct ClnRegTestLnNode {
14 pub public_key: PublicKey,
15 pub address: RegTestLnNodeAddress,
16 pub ca_cert_path: PathBuf,
17 pub client_cert_path: PathBuf,
18 pub client_key_path: PathBuf,
19 pub sni: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct LndRegTestLnNode {
24 pub public_key: PublicKey,
25 pub address: RegTestLnNodeAddress,
26 pub tls_cert_path: PathBuf,
27 pub macaroon_path: PathBuf,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub enum RegTestLnNodeAddress {
32 Inet(SocketAddr),
33 Path(Vec<u8>),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum RegTestLnNode {
38 Cln(ClnRegTestLnNode),
39 Lnd(LndRegTestLnNode),
40}
41
42impl RegTestLnNode {
43 pub fn public_key(&self) -> &PublicKey {
44 match self {
45 RegTestLnNode::Cln(cln) => &cln.public_key,
46 RegTestLnNode::Lnd(lnd) => &lnd.public_key,
47 }
48 }
49
50 pub fn address(&self) -> &RegTestLnNodeAddress {
51 match self {
52 RegTestLnNode::Cln(cln) => &cln.address,
53 RegTestLnNode::Lnd(lnd) => &lnd.address,
54 }
55 }
56
57 pub fn kind(&self) -> &'static str {
58 match self {
59 RegTestLnNode::Cln(_) => "cln",
60 RegTestLnNode::Lnd(_) => "lnd",
61 }
62 }
63}
64
65pub fn get_backends() -> anyhow::Result<Vec<RegTestLnNode>> {
66 if env::var(SKIP_INTEGRATION_TESTS_ENV).is_ok_and(|s| s.to_lowercase() == "true") {
67 eprintln!("⚠️ WARNING: {SKIP_INTEGRATION_TESTS_ENV} is true, skipping tests");
68 return Ok(Vec::new());
69 }
70
71 match get_backends_path() {
72 None => {
73 panic!(
74 "
75
76❌❌❌ ERROR ❌❌❌
77
78{CREDENTIALS_PATH_ENV} is not set. Do one of:
79
801. configure test environment (see testing/README.md) and set {CREDENTIALS_PATH_ENV}
812. set env {SKIP_INTEGRATION_TESTS_ENV}=true to skip integration tests
82
83❌❌❌ ERROR ❌❌❌
84
85 "
86 );
87 }
88 Some(base_path) => get_backends_from_path(base_path),
89 }
90}
91
92#[derive(Copy, Clone, Debug)]
93pub enum RegTestLnNodeType {
94 Cln,
95 Lnd,
96}
97
98fn get_backends_from_path(base_path: PathBuf) -> anyhow::Result<Vec<RegTestLnNode>> {
99 let entries = fs::read_dir(&base_path)
100 .with_context(|| format!("reading directory {}", base_path.display()))?;
101
102 let mut backends = Vec::new();
103
104 for entry in entries {
105 let entry = entry
106 .with_context(|| format!("reading directory entry in {}", base_path.display(),))?;
107
108 let path = entry.path();
109
110 if !path.is_dir() {
111 continue;
112 }
113
114 let dir_name = match path.file_name() {
115 Some(name) => match name.to_str() {
116 Some(s) => s,
117 None => continue, },
119 None => continue,
120 };
121
122 let node_type = if dir_name.starts_with("cln") {
123 RegTestLnNodeType::Cln
124 } else if dir_name.starts_with("lnd") {
125 RegTestLnNodeType::Lnd
126 } else {
127 continue;
128 };
129
130 let node_id_path = path.join("node_id");
132 let node_id_str = fs::read_to_string(&node_id_path)
133 .with_context(|| format!("reading node ID from {}", node_id_path.display(),))?;
134
135 let node_id_hex = node_id_str.trim();
136 let node_id_bytes = hex::decode(node_id_hex)
137 .with_context(|| format!("decoding {} node ID to hex", node_id_path.display(),))?;
138
139 let public_key = PublicKey::from_slice(&node_id_bytes).with_context(|| {
140 format!("parsing {} public key from bytes", node_id_path.display(),)
141 })?;
142
143 let address = read_node_address(&path)?;
145
146 let node = match node_type {
147 RegTestLnNodeType::Cln => {
148 RegTestLnNode::Cln(build_cln_node(public_key, address, &path)?)
149 }
150 RegTestLnNodeType::Lnd => {
151 RegTestLnNode::Lnd(build_lnd_node(public_key, address, &path)?)
152 }
153 };
154
155 backends.push(node);
156 }
157
158 Ok(backends)
159}
160
161fn get_backends_path() -> Option<PathBuf> {
162 let _ = dotenvy::dotenv();
163 env::var(CREDENTIALS_PATH_ENV).ok().map(PathBuf::from)
164}
165
166fn read_node_address(node_path: &Path) -> anyhow::Result<RegTestLnNodeAddress> {
167 let address_file = node_path.join("address.txt");
168
169 let address_content = fs::read_to_string(&address_file)
170 .with_context(|| format!("reading {} address", address_file.display(),))?;
171
172 let address_str = address_content.trim();
173 let socket_addr = SocketAddr::from_str(address_str).with_context(|| {
174 format!(
175 "parsing {} address '{}'",
176 address_file.display(),
177 address_str
178 )
179 })?;
180
181 Ok(RegTestLnNodeAddress::Inet(socket_addr))
182}
183
184fn build_cln_node(
185 public_key: PublicKey,
186 address: RegTestLnNodeAddress,
187 node_path: &Path,
188) -> anyhow::Result<ClnRegTestLnNode> {
189 let ca_cert_path = node_path.join("ca.pem");
190 let ca_cert_path = ca_cert_path
191 .canonicalize()
192 .with_context(|| format!("canonicalizing CLN CA cert path {}", ca_cert_path.display(),))?;
193 let client_cert_path = node_path.join("client.pem");
194 let client_cert_path = client_cert_path.canonicalize().with_context(|| {
195 format!(
196 "canonicalizing CLN client cert path {}",
197 client_cert_path.display(),
198 )
199 })?;
200 let client_key_path = node_path.join("client-key.pem");
201 let client_key_path = client_key_path.canonicalize().with_context(|| {
202 format!(
203 "canonicalizing CLN client key path {}",
204 client_key_path.display(),
205 )
206 })?;
207
208 Ok(ClnRegTestLnNode {
209 public_key,
210 address,
211 ca_cert_path,
212 client_cert_path,
213 client_key_path,
214 sni: "localhost".to_string(),
215 })
216}
217
218fn build_lnd_node(
219 public_key: PublicKey,
220 address: RegTestLnNodeAddress,
221 node_path: &Path,
222) -> anyhow::Result<LndRegTestLnNode> {
223 let tls_cert_path = node_path.join("tls.cert");
224 let tls_cert_path = tls_cert_path.canonicalize().with_context(|| {
225 format!(
226 "canonicalizing LND TLS cert path {}",
227 tls_cert_path.display(),
228 )
229 })?;
230 let macaroon_path = node_path.join("admin.macaroon");
231 let macaroon_path = macaroon_path.canonicalize().with_context(|| {
232 format!(
233 "canonicalizing LND macaroon path {}",
234 macaroon_path.display(),
235 )
236 })?;
237
238 Ok(LndRegTestLnNode {
239 public_key,
240 address,
241 tls_cert_path,
242 macaroon_path,
243 })
244}