1 use crate::cli::LdkUserInfo;
2 use bitcoin::network::constants::Network;
3 use lightning::ln::msgs::SocketAddress;
4 use std::collections::HashMap;
7 use std::path::{Path, PathBuf};
10 pub(crate) fn parse_startup_args() -> Result<LdkUserInfo, ()> {
11 if env::args().len() < 3 {
12 println!("ldk-tutorial-node requires at least 2 arguments: `cargo run [<bitcoind-rpc-username>:<bitcoind-rpc-password>@]<bitcoind-rpc-host>:<bitcoind-rpc-port> ldk_storage_directory_path [<ldk-incoming-peer-listening-port>] [bitcoin-network] [announced-node-name announced-listen-addr*]`");
15 let bitcoind_rpc_info = env::args().skip(1).next().unwrap();
16 let bitcoind_rpc_info_parts: Vec<&str> = bitcoind_rpc_info.rsplitn(2, "@").collect();
18 // Parse rpc auth after getting network for default .cookie location
19 let bitcoind_rpc_path: Vec<&str> = bitcoind_rpc_info_parts[0].split(":").collect();
20 if bitcoind_rpc_path.len() != 2 {
21 println!("ERROR: bad bitcoind RPC path provided");
24 let bitcoind_rpc_host = bitcoind_rpc_path[0].to_string();
25 let bitcoind_rpc_port = bitcoind_rpc_path[1].parse::<u16>().unwrap();
27 let ldk_storage_dir_path = env::args().skip(2).next().unwrap();
29 let mut ldk_peer_port_set = true;
30 let ldk_peer_listening_port: u16 = match env::args().skip(3).next().map(|p| p.parse()) {
33 ldk_peer_port_set = false;
37 ldk_peer_port_set = false;
42 let mut arg_idx = match ldk_peer_port_set {
46 let network: Network = match env::args().skip(arg_idx).next().as_ref().map(String::as_str) {
47 Some("testnet") => Network::Testnet,
48 Some("regtest") => Network::Regtest,
49 Some("signet") => Network::Signet,
51 panic!("Unsupported network provided. Options are: `regtest`, `testnet`, and `signet`. Got {}", net);
53 None => Network::Testnet,
56 let (bitcoind_rpc_username, bitcoind_rpc_password) = if bitcoind_rpc_info_parts.len() == 1 {
57 get_rpc_auth_from_env_vars()
58 .or(get_rpc_auth_from_env_file(None))
59 .or(get_rpc_auth_from_cookie(None, Some(network), None))
61 println!("ERROR: unable to get bitcoind RPC username and password");
62 print_rpc_auth_help();
65 } else if bitcoind_rpc_info_parts.len() == 2 {
66 parse_rpc_auth(bitcoind_rpc_info_parts[1])?
68 println!("ERROR: bad bitcoind RPC URL provided");
72 let ldk_announced_node_name = match env::args().skip(arg_idx + 1).next().as_ref() {
75 panic!("Node Alias can not be longer than 32 bytes");
78 let mut bytes = [0; 32];
79 bytes[..s.len()].copy_from_slice(s.as_bytes());
85 let mut ldk_announced_listen_addr = Vec::new();
87 match env::args().skip(arg_idx + 1).next().as_ref() {
88 Some(s) => match SocketAddress::from_str(s) {
90 ldk_announced_listen_addr.push(sa);
93 Err(_) => panic!("Failed to parse announced-listen-addr into a socket address"),
100 bitcoind_rpc_username,
101 bitcoind_rpc_password,
104 ldk_storage_dir_path,
105 ldk_peer_listening_port,
106 ldk_announced_listen_addr,
107 ldk_announced_node_name,
112 // Default datadir relative to home directory
113 #[cfg(target_os = "windows")]
114 const DEFAULT_BITCOIN_DATADIR: &str = "AppData/Roaming/Bitcoin";
115 #[cfg(target_os = "linux")]
116 const DEFAULT_BITCOIN_DATADIR: &str = ".bitcoin";
117 #[cfg(target_os = "macos")]
118 const DEFAULT_BITCOIN_DATADIR: &str = "Library/Application Support/Bitcoin";
120 // Environment variable/.env keys
121 const BITCOIND_RPC_USER_KEY: &str = "RPC_USER";
122 const BITCOIND_RPC_PASSWORD_KEY: &str = "RPC_PASSWORD";
124 fn print_rpc_auth_help() {
125 // Get the default data directory
126 let home_dir = env::home_dir()
128 .map(|ref p| p.to_str())
132 let data_dir = format!("{}/{}", home_dir, DEFAULT_BITCOIN_DATADIR);
133 println!("To provide the bitcoind RPC username and password, you can either:");
135 "1. Provide the username and password as the first argument to this program in the format: \
136 <bitcoind-rpc-username>:<bitcoind-rpc-password>@<bitcoind-rpc-host>:<bitcoind-rpc-port>"
138 println!("2. Provide <bitcoind-rpc-username>:<bitcoind-rpc-password> in a .cookie file in the default \
139 bitcoind data directory (automatically created by bitcoind on startup): `{}`", data_dir);
141 "3. Set the {} and {} environment variables",
142 BITCOIND_RPC_USER_KEY, BITCOIND_RPC_PASSWORD_KEY
145 "4. Provide {} and {} fields in a .env file in the current directory",
146 BITCOIND_RPC_USER_KEY, BITCOIND_RPC_PASSWORD_KEY
150 fn parse_rpc_auth(rpc_auth: &str) -> Result<(String, String), ()> {
151 let rpc_auth_info: Vec<&str> = rpc_auth.split(':').collect();
152 if rpc_auth_info.len() != 2 {
153 println!("ERROR: bad bitcoind RPC username/password combo provided");
156 let rpc_username = rpc_auth_info[0].to_string();
157 let rpc_password = rpc_auth_info[1].to_string();
158 Ok((rpc_username, rpc_password))
162 data_dir: Option<(&str, bool)>, network: Option<Network>, cookie_file_name: Option<&str>,
163 ) -> Result<PathBuf, ()> {
164 let data_dir_path = match data_dir {
165 Some((dir, true)) => env::home_dir().ok_or(())?.join(dir),
166 Some((dir, false)) => PathBuf::from(dir),
167 None => env::home_dir().ok_or(())?.join(DEFAULT_BITCOIN_DATADIR),
170 let data_dir_path_with_net = match network {
171 Some(Network::Testnet) => data_dir_path.join("testnet3"),
172 Some(Network::Regtest) => data_dir_path.join("regtest"),
173 Some(Network::Signet) => data_dir_path.join("signet"),
177 let cookie_path = data_dir_path_with_net.join(cookie_file_name.unwrap_or(".cookie"));
182 fn get_rpc_auth_from_cookie(
183 data_dir: Option<(&str, bool)>, network: Option<Network>, cookie_file_name: Option<&str>,
184 ) -> Result<(String, String), ()> {
185 let cookie_path = get_cookie_path(data_dir, network, cookie_file_name)?;
186 let cookie_contents = fs::read_to_string(cookie_path).or(Err(()))?;
187 parse_rpc_auth(&cookie_contents)
190 fn get_rpc_auth_from_env_vars() -> Result<(String, String), ()> {
191 if let (Ok(username), Ok(password)) =
192 (env::var(BITCOIND_RPC_USER_KEY), env::var(BITCOIND_RPC_PASSWORD_KEY))
194 Ok((username, password))
200 fn get_rpc_auth_from_env_file(env_file_name: Option<&str>) -> Result<(String, String), ()> {
201 let env_file_map = parse_env_file(env_file_name)?;
202 if let (Some(username), Some(password)) =
203 (env_file_map.get(BITCOIND_RPC_USER_KEY), env_file_map.get(BITCOIND_RPC_PASSWORD_KEY))
205 Ok((username.to_string(), password.to_string()))
211 fn parse_env_file(env_file_name: Option<&str>) -> Result<HashMap<String, String>, ()> {
212 // Default .env file name is .env
213 let env_file_name = match env_file_name {
214 Some(filename) => filename,
219 let env_file_path = Path::new(env_file_name);
220 let env_file_contents = fs::read_to_string(env_file_path).or(Err(()))?;
222 // Collect key-value pairs from .env file into a map
223 let mut env_file_map: HashMap<String, String> = HashMap::new();
224 for line in env_file_contents.lines() {
225 let line_parts: Vec<&str> = line.splitn(2, '=').collect();
226 if line_parts.len() != 2 {
227 println!("ERROR: bad .env file format");
230 env_file_map.insert(line_parts[0].to_string(), line_parts[1].to_string());
240 const TEST_ENV_FILE: &str = "test_data/test_env_file";
241 const TEST_ENV_FILE_BAD: &str = "test_data/test_env_file_bad";
242 const TEST_ABSENT_FILE: &str = "nonexistent_file";
243 const TEST_DATA_DIR: &str = "test_data";
244 const TEST_COOKIE: &str = "test_cookie";
245 const TEST_COOKIE_BAD: &str = "test_cookie_bad";
246 const EXPECTED_USER: &str = "testuser";
247 const EXPECTED_PASSWORD: &str = "testpassword";
250 fn test_parse_rpc_auth_success() {
251 let (username, password) = parse_rpc_auth("testuser:testpassword").unwrap();
252 assert_eq!(username, EXPECTED_USER);
253 assert_eq!(password, EXPECTED_PASSWORD);
257 fn test_parse_rpc_auth_fail() {
258 let result = parse_rpc_auth("testuser");
259 assert!(result.is_err());
263 fn test_get_cookie_path_success() {
264 let test_cases = vec![
269 env::home_dir().unwrap().join(DEFAULT_BITCOIN_DATADIR).join(".cookie"),
272 Some((TEST_DATA_DIR, true)),
273 Some(Network::Testnet),
275 env::home_dir().unwrap().join(TEST_DATA_DIR).join("testnet3").join(".cookie"),
278 Some((TEST_DATA_DIR, false)),
279 Some(Network::Regtest),
281 PathBuf::from(TEST_DATA_DIR).join("regtest").join(TEST_COOKIE),
284 Some((TEST_DATA_DIR, false)),
285 Some(Network::Signet),
287 PathBuf::from(TEST_DATA_DIR).join("signet").join(".cookie"),
290 Some((TEST_DATA_DIR, false)),
291 Some(Network::Bitcoin),
293 PathBuf::from(TEST_DATA_DIR).join(".cookie"),
297 for (data_dir, network, cookie_file, expected_path) in test_cases {
298 let path = get_cookie_path(data_dir, network, cookie_file).unwrap();
299 assert_eq!(path, expected_path);
304 fn test_get_rpc_auth_from_cookie_success() {
305 let (username, password) = get_rpc_auth_from_cookie(
306 Some((TEST_DATA_DIR, false)),
307 Some(Network::Bitcoin),
311 assert_eq!(username, EXPECTED_USER);
312 assert_eq!(password, EXPECTED_PASSWORD);
316 fn test_get_rpc_auth_from_cookie_fail() {
317 let result = get_rpc_auth_from_cookie(
318 Some((TEST_DATA_DIR, false)),
319 Some(Network::Bitcoin),
320 Some(TEST_COOKIE_BAD),
322 assert!(result.is_err());
326 fn test_parse_env_file_success() {
327 let env_file_map = parse_env_file(Some(TEST_ENV_FILE)).unwrap();
328 assert_eq!(env_file_map.get(BITCOIND_RPC_USER_KEY).unwrap(), EXPECTED_USER);
329 assert_eq!(env_file_map.get(BITCOIND_RPC_PASSWORD_KEY).unwrap(), EXPECTED_PASSWORD);
333 fn test_parse_env_file_fail() {
334 let env_file_map = parse_env_file(Some(TEST_ENV_FILE_BAD));
335 assert!(env_file_map.is_err());
337 // Make sure the test file doesn't exist
338 assert!(!Path::new(TEST_ABSENT_FILE).exists());
339 let env_file_map = parse_env_file(Some(TEST_ABSENT_FILE));
340 assert!(env_file_map.is_err());
344 fn test_get_rpc_auth_from_env_file_success() {
345 let (username, password) = get_rpc_auth_from_env_file(Some(TEST_ENV_FILE)).unwrap();
346 assert_eq!(username, EXPECTED_USER);
347 assert_eq!(password, EXPECTED_PASSWORD);
351 fn test_get_rpc_auth_from_env_file_fail() {
352 let rpc_user_and_password = get_rpc_auth_from_env_file(Some(TEST_ABSENT_FILE));
353 assert!(rpc_user_and_password.is_err());
357 fn test_get_rpc_auth_from_env_vars_success() {
358 env::set_var(BITCOIND_RPC_USER_KEY, EXPECTED_USER);
359 env::set_var(BITCOIND_RPC_PASSWORD_KEY, EXPECTED_PASSWORD);
360 let (username, password) = get_rpc_auth_from_env_vars().unwrap();
361 assert_eq!(username, EXPECTED_USER);
362 assert_eq!(password, EXPECTED_PASSWORD);