1use clap::{ErrorKind, Parser};
2use lightning_signer::bitcoin::secp256k1::PublicKey;
3use lightning_signer::bitcoin::Network;
4use lightning_signer::policy::filter::{FilterResult, FilterRule};
5use lightning_signer::policy::simple_validator::OptionizedSimplePolicy;
6use lightning_signer::util::velocity::{VelocityControlIntervalType, VelocityControlSpec};
7use std::ffi::OsStr;
8use std::net::{IpAddr, Ipv4Addr};
9use std::path::PathBuf;
10use std::process::exit;
11use std::sync::{Arc, Mutex, MutexGuard};
12use std::{env, fs};
13use toml::value::{Table, Value};
14use url::Url;
15
16pub const NETWORK_NAMES: &[&str] = &["testnet", "regtest", "signet", "bitcoin"];
18
19pub const CLAP_NETWORK_URL_MAPPING: &[(&str, Option<&str>, Option<&str>)] = &[
21 ("network", Some("bitcoin"), Some("http://user:pass@127.0.0.1:8332")),
22 ("network", Some("testnet"), Some("http://user:pass@127.0.0.1:18332")),
23 ("network", Some("regtest"), Some("http://user:pass@127.0.0.1:18443")),
24 ("network", Some("signet"), Some("http://user:pass@127.0.0.1:18443")),
25];
26
27pub const DEFAULT_DIR: &str = ".lightning-signer";
28pub const RPC_SERVER_PORT: u16 = 8011;
29pub const RPC_SERVER_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
30pub const RPC_SERVER_ENDPOINT: &'static str = "http://127.0.0.1:8011";
31
32pub trait HasSignerArgs {
33 fn signer_args(&self) -> &SignerArgs;
34}
35
36#[derive(Parser, Debug)]
38#[clap(about, long_about = None)]
39pub struct InitialArgs {
40 #[clap(
41 short = 'f',
42 long,
43 value_parser,
44 help = "configuration file - MUST be the first argument",
45 value_name = "FILE"
46 )]
47 config: Option<String>,
48}
49
50#[derive(Parser, Debug)]
52#[clap(about, long_about = None)]
53pub struct SignerArgs {
54 #[clap(flatten)]
55 initial_args: InitialArgs,
56
57 #[clap(long, help = "print git desc version and exit")]
58 pub git_desc: bool,
59
60 #[clap(long, help = "LSS RPC endpoint")]
61 pub lss: Option<Url>,
62
63 #[clap(long, help = "dump LSS contents and exit")]
64 pub dump_lss: bool,
65
66 #[clap(long, help = "initialize LSS from local storage and exit. LSS must be empty.")]
67 pub init_lss: bool,
68
69 #[clap(long, help = "dump local storage contents and exit")]
70 pub dump_storage: bool,
71
72 #[clap(
73 long,
74 value_parser,
75 help = "set the logging level",
76 value_name = "LEVEL",
77 default_value = "info",
78 possible_values = &["off", "error", "warn", "info", "debug", "trace"],
79 )]
80 pub log_level: String,
81
82 #[clap(short, long, value_parser, help = "data directory", value_name = "DIR")]
83 pub datadir: Option<String>,
84
85 #[clap(short, long, value_parser,
86 value_name = "NETWORK",
87 possible_values = NETWORK_NAMES,
88 default_value = NETWORK_NAMES[0]
89 )]
90 pub network: Network,
91
92 #[clap(
93 long,
94 value_parser,
95 help = "use integration test mode, reading/writing hsm_secret from CWD"
96 )]
97 pub integration_test: bool,
98
99 #[clap(
100 long,
101 value_parser,
102 help = "block explorer/bitcoind RPC endpoint - used for broadcasting recovery transactions",
103 default_value_ifs(CLAP_NETWORK_URL_MAPPING),
104 value_name = "URL"
105 )]
106 pub recover_rpc: Option<Url>,
107
108 #[clap(
109 long,
110 value_parser,
111 help = "block explorer type - used for broadcasting recovery transactions",
112 value_name = "TYPE",
113 default_value = "bitcoind",
114 possible_values = &["bitcoind", "esplora"]
115 )]
116 pub recover_type: String,
117
118 #[clap(
119 long,
120 value_parser,
121 help = "recover funds to the given address. By default, l2 funds are recovered (force-close), but you can also recover l1 funds by specifying --recover-l1-range. You can also perform a dry-run by specifying --recover-to=none.",
122 value_name = "BITCOIN_ADDRESS"
123 )]
124 pub recover_to: Option<String>,
125
126 #[clap(
127 long,
128 value_parser,
129 help = "recover l1 funds by sweeping BIP32 addresses up to the given derivation index",
130 value_name = "RANGE"
131 )]
132 pub recover_l1_range: Option<u32>,
133
134 #[clap(long, value_parser=parse_velocity_control_spec, help = "global velocity control e.g. hour:10000 (satoshi)")]
135 pub velocity_control: Option<VelocityControlSpec>,
136
137 #[clap(long, value_parser=parse_velocity_control_spec, help = "fee velocity control e.g. hour:10000 (satoshi)")]
138 pub fee_velocity_control: Option<VelocityControlSpec>,
139
140 #[clap(long, value_parser=parse_filter_rule, help = "policy filter rule, e.g. 'policy-channel-safe-mode:warn' or 'policy-channel-*:error'")]
141 pub policy_filter: Vec<FilterRule>,
142
143 #[clap(
144 long,
145 help = "rpc server's bind address",
146 default_value_t = RPC_SERVER_ADDRESS,
147 value_parser
148 )]
149 pub rpc_server_address: IpAddr,
150
151 #[clap(
152 long,
153 help = "rpc server's port",
154 default_value_t = RPC_SERVER_PORT,
155 value_parser
156 )]
157 pub rpc_server_port: u16,
158
159 #[clap(long, help = "rpc server admin username", value_parser)]
160 pub rpc_user: Option<String>,
161
162 #[clap(long, help = "rpc server admin password", value_parser)]
163 pub rpc_pass: Option<String>,
164
165 #[clap(long, help = "rpc server admin cookie file path", value_parser)]
166 pub rpc_cookie: Option<PathBuf>,
167
168 #[clap(short, help = "public key of trusted TXO oracle", value_parser)]
169 pub trusted_oracle_pubkey: Vec<PublicKey>,
170
171 #[clap(skip)]
172 pub policy: OptionizedSimplePolicy,
173}
174
175impl HasSignerArgs for SignerArgs {
176 fn signer_args(&self) -> &SignerArgs {
177 self
178 }
179}
180
181pub fn parse_args_and_config<A: Parser + HasSignerArgs>(bin_name: &str) -> A {
182 let env_args = env::args().collect::<Vec<_>>();
183 parse_args_and_config_from(bin_name, &env_args).unwrap_or_else(|e| match e.kind() {
184 clap::ErrorKind::DisplayVersion => exit(0), _ => e.exit(),
186 })
187}
188
189#[derive(Clone)]
190struct ConfigIterator {
191 args_stack: Arc<Mutex<Vec<Vec<String>>>>,
192}
193
194impl ConfigIterator {
195 fn new(args: &[String]) -> Self {
196 assert!(args.len() > 0, "at least one arg");
197 ConfigIterator { args_stack: Arc::new(Mutex::new(vec![args.iter().cloned().collect()])) }
198 }
199
200 fn do_next(args_stack: &mut MutexGuard<Vec<Vec<String>>>) -> Option<String> {
201 loop {
202 if args_stack.is_empty() {
203 return None;
204 }
205 let args = &mut args_stack[0];
206 if !args.is_empty() {
207 let arg = args.remove(0);
208 return Some(arg);
209 }
210 args_stack.remove(0);
211 }
212 }
213}
214
215impl Iterator for ConfigIterator {
216 type Item = String;
217
218 fn next(&mut self) -> Option<String> {
219 let mut args_stack = self.args_stack.lock().unwrap();
220 let arg = Self::do_next(&mut args_stack);
221 if let Some(arg) = arg {
222 if arg.starts_with("--config=") {
223 let path = arg.split('=').nth(1).unwrap();
224 let configs = toml_to_configs(path.as_ref());
225 args_stack.insert(0, configs);
226 return Self::do_next(&mut args_stack);
227 } else if arg == "--config" || arg == "-f" {
228 let path_opt = Self::do_next(&mut args_stack);
229 if let Some(path) = path_opt {
230 let configs = toml_to_configs(path.as_ref());
231 args_stack.insert(0, configs);
232 return Self::do_next(&mut args_stack);
233 } else {
234 println!("--config must be followed by a path");
235 return Some(arg);
237 }
238 }
239 return Some(arg);
240 } else {
241 return None;
242 }
243 }
244}
245
246pub fn parse_args_and_config_from<A: Parser + HasSignerArgs>(
247 bin_name: &str,
248 env_args: &[String],
249) -> Result<A, clap::Error> {
250 let args_iter = ConfigIterator::new(env_args);
251 let args = A::try_parse_from(args_iter)?;
252
253 if args.signer_args().git_desc {
255 println!("{} git_desc={}", bin_name, crate::GIT_DESC);
256 return Err(clap::Error::raw(ErrorKind::DisplayVersion, ""));
258 }
259
260 Ok(args)
261}
262
263fn toml_to_configs(path: &OsStr) -> Vec<String> {
264 let contents = fs::read_to_string(path).unwrap();
265 let config: Table = toml::from_str(contents.as_str()).unwrap();
266 let configs = config
267 .into_iter()
268 .flat_map(|(k, value)| {
269 let vals = convert_toml_value(k, value);
270 vals.into_iter()
271 })
272 .map(|(k, v)| format!("--{}={}", k, v).to_string())
273 .collect();
274 configs
275}
276
277fn convert_toml_value(key: String, value: Value) -> Vec<(String, String)> {
278 match value {
279 Value::String(s) => vec![(key, s)],
280 Value::Integer(v) => vec![(key, v.to_string())],
281 Value::Float(v) => vec![(key, v.to_string())],
282 Value::Boolean(v) => vec![(key, v.to_string())],
283 Value::Datetime(v) => vec![(key, v.to_string())],
284 Value::Array(a) =>
285 a.into_iter().flat_map(|v| convert_toml_value(key.clone(), v)).collect::<Vec<_>>(),
286 Value::Table(_) => vec![],
287 }
288}
289
290fn parse_velocity_control_spec(spec: &str) -> Result<VelocityControlSpec, String> {
291 let mut parts = spec.splitn(2, ':');
292 let interval_type_str = parts.next().ok_or("missing duration")?;
293 let interval_type = match interval_type_str {
294 "hour" => VelocityControlIntervalType::Hourly,
295 "day" => VelocityControlIntervalType::Daily,
296 "unlimited" => return Ok(VelocityControlSpec::UNLIMITED),
297 _ => return Err(format!("unknown interval type: {}", interval_type_str)),
298 };
299 let limit: u64 = parts
300 .next()
301 .ok_or("missing limit")?
302 .to_string()
303 .parse()
304 .map_err(|_| "non-integer limit")?;
305 Ok(VelocityControlSpec { interval_type, limit_msat: limit })
306}
307
308fn parse_filter_rule(spec: &str) -> Result<FilterRule, String> {
309 let mut parts = spec.splitn(2, ':');
310 let mut tag: String = parts.next().ok_or("missing filter")?.to_string();
311 let is_prefix = if tag.ends_with('*') {
312 tag.pop();
313 true
314 } else {
315 false
316 };
317 let action_str = parts.next().ok_or("missing level")?;
318 let action = match action_str {
319 "error" => FilterResult::Error,
320 "warn" => FilterResult::Warn,
321 _ => return Err(format!("unknown filter action {}", action_str)),
322 };
323 Ok(FilterRule { tag, action, is_prefix })
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use std::io::Write;
330
331 #[test]
332 fn parse_velocity_control_spec_test() {
333 match parse_velocity_control_spec("hour:100").unwrap().interval_type {
334 VelocityControlIntervalType::Hourly => {}
335 _ => panic!("unexpected interval type"),
336 }
337 match parse_velocity_control_spec("day:100").unwrap().interval_type {
338 VelocityControlIntervalType::Daily => {}
339 _ => panic!("unexpected interval type"),
340 }
341 match parse_velocity_control_spec("unlimited").unwrap().interval_type {
342 VelocityControlIntervalType::Unlimited => {}
343 _ => panic!("unexpected interval type"),
344 }
345 assert!(parse_velocity_control_spec("hour").is_err());
346 assert!(parse_velocity_control_spec("hour:").is_err());
347 assert!(parse_velocity_control_spec("hour:foo").is_err());
348 assert!(parse_velocity_control_spec("foo:100").is_err());
349 }
350
351 #[test]
352 fn parse_filter_rule_test() {
353 assert!(parse_filter_rule("policy-channel-safe-mode:warn").is_ok());
354 assert!(parse_filter_rule("policy-channel-safe-mode:foo").is_err());
355 assert!(parse_filter_rule("policy-channel-safe-mode").is_err());
356 assert!(parse_filter_rule("policy-channel-safe-mode:").is_err());
357 assert!(parse_filter_rule("policy-channel-safe-mode:*").is_err());
358 assert!(parse_filter_rule("policy-channel-safe-mode-*:warn").is_ok());
359 }
360
361 #[test]
362 fn git_desc_test() {
363 let env_args: Vec<String> =
364 vec!["vlsd2", "--git-desc"].into_iter().map(|s| s.to_string()).collect();
365 let args_res: Result<SignerArgs, _> = parse_args_and_config_from("", &env_args);
366 assert!(args_res.is_err());
367 }
368
369 #[test]
370 fn clap_test() {
371 let env_args: Vec<String> = vec!["vlsd2"].into_iter().map(|s| s.to_string()).collect();
372 let args: SignerArgs = parse_args_and_config_from("", &env_args).unwrap();
373 assert!(args.policy_filter.is_empty());
374 assert!(args.velocity_control.is_none());
375 assert!(args.fee_velocity_control.is_none());
376
377 let env_args: Vec<String> = vec![
378 "vlsd2",
379 "--datadir=/tmp/vlsd2",
380 "--network=regtest",
381 "--integration-test",
382 "--recover-rpc=http://localhost:3000",
383 "--policy-filter",
384 "policy-channel-safe-mode:warn",
385 "--velocity-control",
386 "hour:100",
387 "--recover-to",
388 "abc123",
389 "--recover-l1-range",
390 "100",
391 "--rpc-server-address",
392 "127.0.0.1",
393 "--rpc-server-port",
394 "8011",
395 ]
396 .into_iter()
397 .map(|s| s.to_string())
398 .collect();
399 let args: SignerArgs = parse_args_and_config_from("", &env_args).unwrap();
400 assert_eq!(args.datadir.unwrap(), "/tmp/vlsd2");
401 assert_eq!(args.policy_filter.len(), 1);
402 assert!(args.velocity_control.is_some());
403 assert_eq!(args.network, Network::Regtest);
404 assert!(args.integration_test);
405 assert_eq!(args.recover_rpc.unwrap().as_str(), "http://localhost:3000/");
406 assert_eq!(args.recover_type, "bitcoind");
407 assert_eq!(args.recover_to.unwrap().as_str(), "abc123");
408 assert_eq!(args.recover_l1_range, Some(100));
409 assert_eq!(args.rpc_server_address, IpAddr::V4(Ipv4Addr::LOCALHOST));
410 assert_eq!(args.rpc_server_port, 8011);
411 }
412
413 #[test]
414 fn clap_with_config_file_test() {
415 let mut file = tempfile::NamedTempFile::new().unwrap();
416 writeln!(
417 file,
418 "datadir = \"/tmp/vlsd2\"\n\
419 velocity-control = \"day:222\"\n\
420 network = \"regtest\"\n\
421 "
422 )
423 .unwrap();
424 let env_args: Vec<String> =
425 vec!["vlsd2", "--config", file.path().to_str().unwrap(), "--network=bitcoin"]
426 .into_iter()
427 .map(|s| s.to_string())
428 .collect();
429 let args: SignerArgs = parse_args_and_config_from("", &env_args).unwrap();
430 assert_eq!(args.datadir.unwrap(), "/tmp/vlsd2");
431 assert!(args.velocity_control.is_some());
432 assert_eq!(args.network, Network::Bitcoin);
434
435 let env_args: Vec<String> =
436 vec!["vlsd2", "--network=bitcoin", "--config", file.path().to_str().unwrap()]
437 .into_iter()
438 .map(|s| s.to_string())
439 .collect();
440 let args: SignerArgs = parse_args_and_config_from("", &env_args).unwrap();
441 assert_eq!(args.datadir.unwrap(), "/tmp/vlsd2");
442 assert!(args.velocity_control.is_some());
443 assert_eq!(args.network, Network::Regtest);
445 }
446}