From 4747a28bf5c7e573a5a9637650e92f48f41f79a3 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor <casey@rodarmor.com> Date: Thu, 7 Mar 2024 16:11:03 -0800 Subject: [PATCH] Load config from default data dir and configure `ord env ` using config (#3240) --- docs/src/guides/settings.md | 13 ++- src/lib.rs | 1 - src/settings.rs | 210 ++++++++++++++++++++++++------------ src/subcommand/env.rs | 47 +++----- src/subcommand/wallet.rs | 14 ++- src/test.rs | 1 + tests/settings.rs | 15 +++ 7 files changed, 191 insertions(+), 110 deletions(-) diff --git a/docs/src/guides/settings.md b/docs/src/guides/settings.md index 90587d2a1a..453f28a8a4 100644 --- a/docs/src/guides/settings.md +++ b/docs/src/guides/settings.md @@ -8,10 +8,15 @@ The command line takes precedence over environment variables, which take precedence over the configuration file, which takes precedence over defaults. The path to the configuration file can be given with `--config <CONFIG_PATH>`. -`ord` will error if `<CONFIG_PATH>` doesn't exist. The path to a configuration -directory can be given with `--config-dir <CONFIG_DIR_PATH>`, in which case the -config path is `<CONFIG_DIR_PATH>/ord.yaml`. It is not an error if -`<CONFIG_DIR_PATH>/ord.yaml` does not exist. +`ord` will error if `<CONFIG_PATH>` doesn't exist. + +The path to a directory containing a configuration file name named `ord.yaml` +can be given with `--config-dir <CONFIG_DIR_PATH>` or `--data-dir +<DATA_DIR_PATH>` in which case the config path is `<CONFIG_DIR_PATH>/ord.yaml` +or `<DATA_DIR_PATH>/ord.yaml`. It is not an error if it does not exist. + +If none of `--config`, `--config-dir`, or `--data-dir` are given, and a file +named `ord.yaml` exists in the default data directory, it will be loaded. For a setting named `--setting-name` on the command line, the environment variable will be named `ORD_SETTING_NAME`, and the config file field will be diff --git a/src/lib.rs b/src/lib.rs index 5775ae7e85..8ab414a50d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,7 +190,6 @@ pub fn parse_ord_server_args(args: &str) -> (Settings, subcommand::server::Serve vec![("INTEGRATION_TEST".into(), "1".into())] .into_iter() .collect(), - Default::default(), ) .unwrap(), server, diff --git a/src/settings.rs b/src/settings.rs index 9953c15f2d..34d8e41309 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -9,6 +9,8 @@ pub struct Settings { bitcoin_rpc_username: Option<String>, chain: Option<Chain>, commit_interval: Option<usize>, + config: Option<PathBuf>, + config_dir: Option<PathBuf>, cookie_file: Option<PathBuf>, data_dir: Option<PathBuf>, first_inscription_height: Option<u32>, @@ -23,39 +25,13 @@ pub struct Settings { integration_test: bool, no_index_inscriptions: bool, server_password: Option<String>, + server_url: Option<String>, server_username: Option<String>, } impl Settings { pub(crate) fn load(options: Options) -> Result<Settings> { - let config_path = match &options.config { - Some(path) => Some(path.into()), - None => match &options.config_dir { - Some(dir) => { - let path = dir.join("ord.yaml"); - if path.exists() { - Some(path) - } else { - None - } - } - None => None, - }, - }; - - let config = match config_path { - Some(config_path) => serde_yaml::from_reader(File::open(&config_path).context(anyhow!( - "failed to open config file `{}`", - config_path.display() - ))?) - .context(anyhow!( - "failed to deserialize config file `{}`", - config_path.display() - ))?, - None => Settings::default(), - }; - - let mut env: BTreeMap<String, String> = BTreeMap::new(); + let mut env = BTreeMap::<String, String>::new(); for (var, value) in env::vars_os() { let Some(var) = var.to_str() else { @@ -77,18 +53,39 @@ impl Settings { ); } - Self::merge(options, env, config) + Self::merge(options, env) } - pub(crate) fn merge( - options: Options, - env: BTreeMap<String, String>, - config: Settings, - ) -> Result<Self> { - let settings = Settings::from_options(options) - .or(Settings::from_env(env)?) - .or(config) - .or_defaults()?; + pub(crate) fn merge(options: Options, env: BTreeMap<String, String>) -> Result<Self> { + let settings = Settings::from_options(options).or(Settings::from_env(env)?); + + let config_path = if let Some(path) = &settings.config { + Some(path.into()) + } else { + let path = if let Some(dir) = settings.config_dir.clone().or(settings.data_dir.clone()) { + dir + } else { + Self::default_data_dir()? + } + .join("ord.yaml"); + + path.exists().then_some(path) + }; + + let config = if let Some(config_path) = config_path { + serde_yaml::from_reader(File::open(&config_path).context(anyhow!( + "failed to open config file `{}`", + config_path.display() + ))?) + .context(anyhow!( + "failed to deserialize config file `{}`", + config_path.display() + ))? + } else { + Settings::default() + }; + + let settings = settings.or(config).or_defaults()?; match ( &settings.bitcoin_rpc_username, @@ -116,6 +113,8 @@ impl Settings { bitcoin_rpc_username: self.bitcoin_rpc_username.or(source.bitcoin_rpc_username), chain: self.chain.or(source.chain), commit_interval: self.commit_interval.or(source.commit_interval), + config: self.config.or(source.config), + config_dir: self.config_dir.or(source.config_dir), cookie_file: self.cookie_file.or(source.cookie_file), data_dir: self.data_dir.or(source.data_dir), first_inscription_height: self @@ -140,6 +139,7 @@ impl Settings { integration_test: self.integration_test || source.integration_test, no_index_inscriptions: self.no_index_inscriptions || source.no_index_inscriptions, server_password: self.server_password.or(source.server_password), + server_url: self.server_url.or(source.server_url), server_username: self.server_username.or(source.server_username), } } @@ -157,6 +157,8 @@ impl Settings { .or(options.testnet.then_some(Chain::Testnet)) .or(options.chain_argument), commit_interval: options.commit_interval, + config: options.config, + config_dir: options.config_dir, cookie_file: options.cookie_file, data_dir: options.data_dir, first_inscription_height: options.first_inscription_height, @@ -171,6 +173,7 @@ impl Settings { integration_test: options.integration_test, no_index_inscriptions: options.no_index_inscriptions, server_password: options.server_password, + server_url: None, server_username: options.server_username, } } @@ -232,6 +235,8 @@ impl Settings { bitcoin_rpc_username: get_string("BITCOIN_RPC_USERNAME"), chain: get_chain("CHAIN")?, commit_interval: get_usize("COMMIT_INTERVAL")?, + config: get_path("CONFIG"), + config_dir: get_path("CONFIG_DIR"), cookie_file: get_path("COOKIE_FILE"), data_dir: get_path("DATA_DIR"), first_inscription_height: get_u32("FIRST_INSCRIPTION_HEIGHT")?, @@ -246,10 +251,40 @@ impl Settings { integration_test: get_bool("INTEGRATION_TEST"), no_index_inscriptions: get_bool("NO_INDEX_INSCRIPTIONS"), server_password: get_string("SERVER_PASSWORD"), + server_url: get_string("SERVER_URL"), server_username: get_string("SERVER_USERNAME"), }) } + pub(crate) fn for_env(dir: &Path, rpc_url: &str, server_url: &str) -> Self { + Self { + bitcoin_data_dir: Some(dir.into()), + bitcoin_rpc_password: None, + bitcoin_rpc_url: Some(rpc_url.into()), + bitcoin_rpc_username: None, + chain: Some(Chain::Regtest), + commit_interval: None, + config: None, + config_dir: None, + cookie_file: None, + data_dir: Some(dir.into()), + first_inscription_height: None, + height_limit: None, + hidden: None, + index: None, + index_cache_size: None, + index_runes: true, + index_sats: true, + index_spent_sats: false, + index_transactions: false, + integration_test: false, + no_index_inscriptions: false, + server_password: None, + server_url: Some(server_url.into()), + server_username: None, + } + } + pub(crate) fn or_defaults(self) -> Result<Self> { let chain = self.chain.unwrap_or_default(); @@ -275,9 +310,7 @@ impl Settings { let data_dir = chain.join_with_data_dir(match &self.data_dir { Some(data_dir) => data_dir.clone(), - None => dirs::data_dir() - .context("could not get data dir")? - .join("ord"), + None => Self::default_data_dir()?, }); let index = match &self.index { @@ -297,6 +330,8 @@ impl Settings { bitcoin_rpc_username: self.bitcoin_rpc_username, chain: Some(chain), commit_interval: Some(self.commit_interval.unwrap_or(5000)), + config: None, + config_dir: None, cookie_file: Some(cookie_file), data_dir: Some(data_dir), first_inscription_height: Some(if self.integration_test { @@ -324,10 +359,19 @@ impl Settings { integration_test: self.integration_test, no_index_inscriptions: self.no_index_inscriptions, server_password: self.server_password, + server_url: self.server_url, server_username: self.server_username, }) } + pub(crate) fn default_data_dir() -> Result<PathBuf> { + Ok( + dirs::data_dir() + .context("could not get data dir")? + .join("ord"), + ) + } + pub(crate) fn bitcoin_credentials(&self) -> Result<Auth> { if let Some((user, pass)) = &self .bitcoin_rpc_username @@ -505,6 +549,10 @@ impl Settings { None => format!("{base_url}/"), } } + + pub(crate) fn server_url(&self) -> Option<&str> { + self.server_url.as_deref() + } } #[cfg(test)] @@ -544,7 +592,6 @@ mod tests { ..Default::default() }, Default::default(), - Default::default(), ) .unwrap_err() .to_string(), @@ -561,7 +608,6 @@ mod tests { ..Default::default() }, Default::default(), - Default::default(), ) .unwrap_err() .to_string(), @@ -850,11 +896,24 @@ mod tests { #[test] fn bitcoin_rpc_and_pass_setting() { + let config = Settings { + bitcoin_rpc_username: Some("config_user".into()), + bitcoin_rpc_password: Some("config_pass".into()), + ..Default::default() + }; + + let tempdir = TempDir::new().unwrap(); + + let config_path = tempdir.path().join("ord.yaml"); + + fs::write(&config_path, serde_yaml::to_string(&config).unwrap()).unwrap(); + assert_eq!( Settings::merge( Options { bitcoin_rpc_username: Some("option_user".into()), bitcoin_rpc_password: Some("option_pass".into()), + config: Some(config_path.clone()), ..Default::default() }, vec![ @@ -863,11 +922,6 @@ mod tests { ] .into_iter() .collect(), - Settings { - bitcoin_rpc_username: Some("config_user".into()), - bitcoin_rpc_password: Some("config_pass".into()), - ..Default::default() - } ) .unwrap() .bitcoin_credentials() @@ -877,18 +931,16 @@ mod tests { assert_eq!( Settings::merge( - Default::default(), + Options { + config: Some(config_path.clone()), + ..Default::default() + }, vec![ ("BITCOIN_RPC_USERNAME".into(), "env_user".into()), ("BITCOIN_RPC_PASSWORD".into(), "env_pass".into()), ] .into_iter() .collect(), - Settings { - bitcoin_rpc_username: Some("config_user".into()), - bitcoin_rpc_password: Some("config_pass".into()), - ..Default::default() - } ) .unwrap() .bitcoin_credentials() @@ -898,13 +950,11 @@ mod tests { assert_eq!( Settings::merge( - Default::default(), - Default::default(), - Settings { - bitcoin_rpc_username: Some("config_user".into()), - bitcoin_rpc_password: Some("config_pass".into()), + Options { + config: Some(config_path), ..Default::default() - } + }, + Default::default(), ) .unwrap() .bitcoin_credentials() @@ -913,7 +963,7 @@ mod tests { ); assert_matches!( - Settings::merge(Default::default(), Default::default(), Default::default(),) + Settings::merge(Default::default(), Default::default()) .unwrap() .bitcoin_credentials() .unwrap(), @@ -935,6 +985,8 @@ mod tests { ("BITCOIN_RPC_USERNAME", "bitcoin username"), ("CHAIN", "signet"), ("COMMIT_INTERVAL", "1"), + ("CONFIG", "config"), + ("CONFIG_DIR", "config dir"), ("COOKIE_FILE", "cookie file"), ("DATA_DIR", "/data/dir"), ("FIRST_INSCRIPTION_HEIGHT", "2"), @@ -949,6 +1001,7 @@ mod tests { ("INTEGRATION_TEST", "1"), ("NO_INDEX_INSCRIPTIONS", "1"), ("SERVER_PASSWORD", "server password"), + ("SERVER_URL", "server url"), ("SERVER_USERNAME", "server username"), ] .into_iter() @@ -964,6 +1017,8 @@ mod tests { bitcoin_rpc_username: Some("bitcoin username".into()), chain: Some(Chain::Signet), commit_interval: Some(1), + config: Some("config".into()), + config_dir: Some("config dir".into()), cookie_file: Some("cookie file".into()), data_dir: Some("/data/dir".into()), first_inscription_height: Some(2), @@ -989,6 +1044,7 @@ mod tests { integration_test: true, no_index_inscriptions: true, server_password: Some("server password".into()), + server_url: Some("server url".into()), server_username: Some("server username".into()), } ); @@ -1006,6 +1062,8 @@ mod tests { "--bitcoin-rpc-username=bitcoin username", "--chain=signet", "--commit-interval=1", + "--config=config", + "--config-dir=config dir", "--cookie-file=cookie file", "--data-dir=/data/dir", "--first-inscription-height=2", @@ -1030,6 +1088,8 @@ mod tests { bitcoin_rpc_username: Some("bitcoin username".into()), chain: Some(Chain::Signet), commit_interval: Some(1), + config: Some("config".into()), + config_dir: Some("config dir".into()), cookie_file: Some("cookie file".into()), data_dir: Some("/data/dir".into()), first_inscription_height: Some(2), @@ -1044,6 +1104,7 @@ mod tests { integration_test: true, no_index_inscriptions: true, server_password: Some("server password".into()), + server_url: None, server_username: Some("server username".into()), } ); @@ -1056,29 +1117,42 @@ mod tests { .map(|(key, value)| (key.into(), value.into())) .collect::<BTreeMap<String, String>>(); - let options = Options::try_parse_from(["ord", "--index=option"]).unwrap(); - let config = Settings { index: Some("config".into()), ..Default::default() }; + let tempdir = TempDir::new().unwrap(); + + let config_path = tempdir.path().join("ord.yaml"); + + fs::write(&config_path, serde_yaml::to_string(&config).unwrap()).unwrap(); + + let options = + Options::try_parse_from(["ord", "--config", config_path.to_str().unwrap()]).unwrap(); + pretty_assert_eq!( - Settings::merge(Default::default(), Default::default(), config.clone()) + Settings::merge(options.clone(), Default::default()) .unwrap() .index, Some("config".into()), ); pretty_assert_eq!( - Settings::merge(Default::default(), env.clone(), config.clone()) - .unwrap() - .index, + Settings::merge(options, env.clone()).unwrap().index, Some("env".into()), ); + let options = Options::try_parse_from([ + "ord", + "--index=option", + "--config", + config_path.to_str().unwrap(), + ]) + .unwrap(); + pretty_assert_eq!( - Settings::merge(options, env, config.clone()).unwrap().index, + Settings::merge(options, env).unwrap().index, Some("option".into()), ); } diff --git a/src/subcommand/env.rs b/src/subcommand/env.rs index c86924e133..b562c60a63 100644 --- a/src/subcommand/env.rs +++ b/src/subcommand/env.rs @@ -21,9 +21,9 @@ pub(crate) struct Env { #[derive(Serialize)] struct Info { + bitcoin_cli_command: Vec<String>, bitcoind_port: u16, ord_port: u16, - bitcoin_cli_command: Vec<String>, ord_wallet_command: Vec<String>, } @@ -76,19 +76,23 @@ rpcport={bitcoind_port} } } - let ord = std::env::current_exe()?; - let rpc_url = format!("http://localhost:{bitcoind_port}"); + let server_url = format!("http://127.0.0.1:{ord_port}"); + + let config = absolute.join("ord.yaml"); + + fs::write( + config, + serde_yaml::to_string(&Settings::for_env(&absolute, &rpc_url, &server_url))?, + )?; + + let ord = std::env::current_exe()?; + let _ord = KillOnDrop( Command::new(&ord) - .arg("--regtest") - .arg("--bitcoin-data-dir") - .arg(&absolute) .arg("--data-dir") .arg(&absolute) - .arg("--bitcoin-rpc-url") - .arg(&rpc_url) .arg("server") .arg("--polling-interval=100ms") .arg("--http-port") @@ -98,17 +102,10 @@ rpcport={bitcoind_port} thread::sleep(Duration::from_millis(250)); - let server_url = format!("http://127.0.0.1:{ord_port}"); - if !absolute.join("regtest/wallets/ord").try_exists()? { let status = Command::new(&ord) - .arg("--regtest") - .arg("--bitcoin-data-dir") - .arg(&absolute) .arg("--data-dir") .arg(&absolute) - .arg("--bitcoin-rpc-url") - .arg(&rpc_url) .arg("wallet") .arg("create") .status()?; @@ -116,16 +113,9 @@ rpcport={bitcoind_port} ensure!(status.success(), "failed to create wallet: {status}"); let output = Command::new(&ord) - .arg("--regtest") - .arg("--bitcoin-data-dir") - .arg(&absolute) .arg("--data-dir") .arg(&absolute) - .arg("--bitcoin-rpc-url") - .arg(&rpc_url) .arg("wallet") - .arg("--server-url") - .arg(&server_url) .arg("receive") .output()?; @@ -157,16 +147,9 @@ rpcport={bitcoind_port} bitcoin_cli_command: vec!["bitcoin-cli".into(), format!("-datadir={relative}")], ord_wallet_command: vec![ ord.to_str().unwrap().into(), - "--regtest".into(), - "--bitcoin-data-dir".into(), - relative.clone(), "--data-dir".into(), - relative.clone(), - "--bitcoin-rpc-url".into(), - rpc_url.clone(), + absolute.to_str().unwrap().into(), "wallet".into(), - "--server-url".into(), - server_url.clone(), ], }, )?; @@ -175,12 +158,10 @@ rpcport={bitcoind_port} "{} bitcoin-cli -datadir='{relative}' getblockchaininfo {} -{} --regtest --bitcoin-data-dir '{relative}' --data-dir '{relative}' --bitcoin-rpc-url '{}' wallet --server-url {} balance", +{} --data-dir '{relative}' wallet balance", "Example `bitcoin-cli` command:".blue().bold(), "Example `ord` command:".blue().bold(), ord.display(), - rpc_url, - server_url, ); loop { diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 8518f50b35..59133dee65 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -30,10 +30,9 @@ pub(crate) struct WalletCommand { pub(crate) no_sync: bool, #[arg( long, - default_value = "http://127.0.0.1:80", - help = "Use ord running at <SERVER_URL>." + help = "Use ord running at <SERVER_URL>. [default: http://localhost:80]" )] - pub(crate) server_url: Url, + pub(crate) server_url: Option<Url>, #[command(subcommand)] pub(crate) subcommand: Subcommand, } @@ -81,7 +80,14 @@ impl WalletCommand { self.name.clone(), self.no_sync, settings.clone(), - self.server_url, + self + .server_url + .as_ref() + .map(Url::as_str) + .or(settings.server_url()) + .unwrap_or("http://127.0.0.1:80") + .parse::<Url>() + .context("invalid server URL")?, )?; match self.subcommand { diff --git a/src/test.rs b/src/test.rs index 8c4e623625..5e90c2c453 100644 --- a/src/test.rs +++ b/src/test.rs @@ -7,6 +7,7 @@ pub(crate) use { }, pretty_assertions::assert_eq as pretty_assert_eq, std::iter, + tempfile::TempDir, test_bitcoincore_rpc::TransactionTemplate, unindent::Unindent, }; diff --git a/tests/settings.rs b/tests/settings.rs index 78c2cc2e6e..bfd271d28b 100644 --- a/tests/settings.rs +++ b/tests/settings.rs @@ -12,6 +12,8 @@ fn default() { "bitcoin_rpc_username": null, "chain": "mainnet", "commit_interval": 5000, + "config": null, + "config_dir": null, "cookie_file": ".*\.cookie", "data_dir": ".*", "first_inscription_height": 767430, @@ -26,6 +28,7 @@ fn default() { "integration_test": false, "no_index_inscriptions": false, "server_password": null, + "server_url": null, "server_username": null \} "#, @@ -94,6 +97,18 @@ fn config_is_loaded_from_config_dir() { .run_and_extract_stdout(); } +#[test] +fn config_is_loaded_from_data_dir() { + CommandBuilder::new("settings") + .write("ord.yaml", "chain: regtest") + .stdout_regex( + r#".* + "chain": "regtest", +.*"#, + ) + .run_and_extract_stdout(); +} + #[test] fn env_is_loaded() { CommandBuilder::new("settings")