1mod env_var;
2mod r#macro;
3pub mod observability;
4mod rpc_cookie;
5mod testing;
6mod validation;
7
8pub use rpc_cookie::get_rpc_credentials;
9
10#[cfg(feature = "otlp")]
11mod otlp;
12
13pub use env_var::*;
14pub use testing::*;
15pub use validation::*;
16
17use clap::{arg, AppSettings};
18#[cfg(feature = "main")]
19use clap::{App, Arg, ArgMatches};
20use log::*;
21use std::env;
22use std::fs::File;
23use std::io::{BufRead, BufReader};
24use std::path::Path;
25use time::{macros::format_description, OffsetDateTime};
26use tokio::runtime::{self, Runtime};
27
28pub fn line_filter(line: &str) -> Option<String> {
29 let whitespace_removed = line.trim();
30 if whitespace_removed.is_empty() {
31 return None;
32 }
33 let comment_removed = whitespace_removed.split('#').next()?.trim();
34 if comment_removed.is_empty() {
35 return None;
36 }
37 Some(comment_removed.to_string())
38}
39
40pub fn read_allowlist() -> Vec<String> {
41 if let Ok(allowlist_path) = env::var("REMOTE_SIGNER_ALLOWLIST") {
42 return read_allowlist_path(&allowlist_path);
43 }
44 Vec::new()
45}
46
47pub fn read_allowlist_path(path: &str) -> Vec<String> {
48 let file = File::open(path).expect(format!("open {} failed", path).as_str());
49 let allowlist: Vec<String> =
50 BufReader::new(file).lines().filter_map(|l| line_filter(&l.expect("line"))).collect();
51
52 allowlist
53}
54
55#[cfg(feature = "main")]
56pub fn setup_logging<P: AsRef<Path>>(datadir: P, who: &str, level_arg: &str) {
57 use fern::colors::{Color, ColoredLevelConfig};
58 use std::str::FromStr;
59
60 let level = env::var("RUST_LOG").unwrap_or(level_arg.to_string());
62
63 let who_clone = who.to_string();
65 let logfile = datadir.as_ref().join(format!("{}.log", who));
66 let file_config = fern::Dispatch::new()
67 .format(move |out, message, record| {
68 out.finish(format_args!(
69 "[{} {}/{} {}] {}",
70 tstamp(),
71 who_clone,
72 record.target(),
73 record.level(),
74 message
75 ))
76 })
77 .level(log::LevelFilter::from_str(&level).expect("level"))
78 .level_for("h2", log::LevelFilter::Info)
79 .chain(fern::log_file(logfile).expect("file log config"));
80
81 let who_clone = who.to_string();
83 let colors = ColoredLevelConfig::new().info(Color::Green).error(Color::Red).warn(Color::Yellow);
84 let console_config = fern::Dispatch::new()
85 .format(move |out, message, record| {
86 out.finish(format_args!(
87 "[{} {}/{} {}] {}",
88 tstamp(),
89 who_clone,
90 record.target(),
91 colors.color(record.level()),
92 message
93 ))
94 })
95 .level(log::LevelFilter::from_str(&level).expect("level"))
96 .level_for("h2", log::LevelFilter::Info)
97 .chain(std::io::stdout());
98
99 fern::Dispatch::new().chain(console_config).chain(file_config).apply().expect("log config");
100}
101
102#[cfg(feature = "main")]
103pub fn add_hsmd_args(app: App) -> App {
104 app.setting(AppSettings::NoAutoVersion)
105 .arg(
106 Arg::new("dev-disconnect")
107 .help("ignored dev flag")
108 .long("dev-disconnect")
109 .takes_value(true),
110 )
111 .arg(Arg::new("developer").long("developer").help("ignored dev flag"))
112 .arg(Arg::new("log-io").long("log-io").help("ignored dev flag"))
113 .arg(arg!(--version "show a dummy version"))
114 .arg(Arg::new("git-desc").long("git-desc").help("print git desc version and exit"))
115}
116
117#[cfg(feature = "main")]
118pub fn handle_hsmd_version(matches: &ArgMatches) -> bool {
119 if matches.is_present("version") {
120 let version = vls_cln_version();
122 println!("{}", version);
123 true
124 } else {
125 false
126 }
127}
128
129pub fn create_runtime(thread_name: &str) -> Runtime {
130 let thrname = thread_name.to_string();
131 std::thread::spawn(|| {
132 runtime::Builder::new_multi_thread()
133 .enable_all()
134 .thread_name(thrname)
135 .worker_threads(2) .build()
137 })
138 .join()
139 .expect("runtime join")
140 .expect("runtime")
141}
142
143pub fn should_auto_approve() -> bool {
145 if compare_env_var("VLS_PERMISSIVE", "1") {
146 warn!("VLS_PERMISSIVE: ALL INVOICES, KEYSENDS, AND PAYMENTS AUTOMATICALLY APPROVED");
147 return true;
148 }
149
150 if compare_env_var("VLS_AUTOAPPROVE", "1") {
151 warn!("VLS_AUTOAPPROVE: ALL INVOICES, KEYSENDS, AND PAYMENTS AUTOMATICALLY APPROVED");
152 return true;
153 }
154
155 info!("VLS_ENFORCING: ALL INVOICES, KEYSENDS, AND PAYMENTS REQUIRE APPROVAL");
156 false
157}
158
159pub fn abort_on_panic() {
163 let old = std::panic::take_hook();
164 std::panic::set_hook(Box::new(move |info| {
165 old(info);
166 std::process::abort();
167 }));
168}
169
170#[cfg(feature = "main")]
173pub fn tstamp() -> String {
174 OffsetDateTime::now_utc()
175 .format(format_description!(
176 "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"
177 ))
178 .expect("formatted tstamp")
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use std::io::Write;
185
186 #[test]
187 fn line_filter_test() {
188 assert_eq!(line_filter("#"), None);
189 assert_eq!(
190 line_filter("tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z # comment"),
191 Some("tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z".to_string())
192 );
193 assert_eq!(line_filter(" "), None);
194 assert_eq!(line_filter(" # "), None);
195 assert_eq!(
196 line_filter(" tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z "),
197 Some("tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z".to_string())
198 );
199 }
200
201 #[test]
202 fn read_allowlist_test() {
203 let test_file_content = "\
204 # Sample Allowlist
205 tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z
206 tb1qexampleaddress1234567890123456789012345678
207
208 # Another comment line after blank line
209 ";
210 let mut temp_file = tempfile::NamedTempFile::new().unwrap();
211 write!(temp_file, "{}", test_file_content).unwrap();
212 let allowlist = read_allowlist_path(temp_file.path().to_str().unwrap());
213 assert_eq!(
214 allowlist,
215 vec![
216 "tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z".to_string(),
217 "tb1qexampleaddress1234567890123456789012345678".to_string(),
218 ]
219 );
220 temp_file.close().unwrap();
221 }
222}