ord/
settings.rs

1use {super::*, bitcoincore_rpc::Auth};
2
3#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
4#[serde(default, deny_unknown_fields)]
5pub struct Settings {
6  bitcoin_data_dir: Option<PathBuf>,
7  bitcoin_rpc_password: Option<String>,
8  bitcoin_rpc_url: Option<String>,
9  bitcoin_rpc_username: Option<String>,
10  pub postgres_uri: Option<String>, //Support for postgres database
11  chain: Option<Chain>,
12  commit_interval: Option<usize>,
13  config: Option<PathBuf>,
14  config_dir: Option<PathBuf>,
15  cookie_file: Option<PathBuf>,
16  data_dir: Option<PathBuf>,
17  first_inscription_height: Option<u32>,
18  height_limit: Option<u32>,
19  hidden: Option<HashSet<InscriptionId>>,
20  index: Option<PathBuf>,
21  index_cache_size: Option<usize>,
22  index_runes: bool,
23  index_sats: bool,
24  index_spent_sats: bool,
25  index_transactions: bool,
26  integration_test: bool,
27  no_index_inscriptions: bool,
28  server_password: Option<String>,
29  server_url: Option<String>,
30  server_username: Option<String>,
31}
32
33impl Settings {
34  pub fn load(options: Options) -> Result<Settings> {
35    let mut env = BTreeMap::<String, String>::new();
36
37    for (var, value) in env::vars_os() {
38      let Some(var) = var.to_str() else {
39        continue;
40      };
41
42      let Some(key) = var.strip_prefix("ORD_") else {
43        continue;
44      };
45
46      env.insert(
47        key.into(),
48        value.into_string().map_err(|value| {
49          anyhow!(
50            "environment variable `{var}` not valid unicode: `{}`",
51            value.to_string_lossy()
52          )
53        })?,
54      );
55    }
56
57    Self::merge(options, env)
58  }
59
60  pub(crate) fn merge(options: Options, env: BTreeMap<String, String>) -> Result<Self> {
61    let settings = Settings::from_options(options).or(Settings::from_env(env)?);
62
63    let config_path = if let Some(path) = &settings.config {
64      Some(path.into())
65    } else {
66      let path = if let Some(dir) = settings.config_dir.clone().or(settings.data_dir.clone()) {
67        dir
68      } else {
69        Self::default_data_dir()?
70      }
71      .join("ord.yaml");
72
73      path.exists().then_some(path)
74    };
75
76    let config = if let Some(config_path) = config_path {
77      serde_yaml::from_reader(fs::File::open(&config_path).context(anyhow!(
78        "failed to open config file `{}`",
79        config_path.display()
80      ))?)
81      .context(anyhow!(
82        "failed to deserialize config file `{}`",
83        config_path.display()
84      ))?
85    } else {
86      Settings::default()
87    };
88
89    let settings = settings.or(config).or_defaults()?;
90
91    match (
92      &settings.bitcoin_rpc_username,
93      &settings.bitcoin_rpc_password,
94    ) {
95      (None, Some(_rpc_pass)) => bail!("no bitcoin RPC username specified"),
96      (Some(_rpc_user), None) => bail!("no bitcoin RPC password specified"),
97      _ => {}
98    };
99
100    match (&settings.server_username, &settings.server_password) {
101      (None, Some(_rpc_pass)) => bail!("no username specified"),
102      (Some(_rpc_user), None) => bail!("no password specified"),
103      _ => {}
104    };
105
106    Ok(settings)
107  }
108
109  pub(crate) fn or(self, source: Settings) -> Self {
110    Self {
111      bitcoin_data_dir: self.bitcoin_data_dir.or(source.bitcoin_data_dir),
112      bitcoin_rpc_password: self.bitcoin_rpc_password.or(source.bitcoin_rpc_password),
113      bitcoin_rpc_url: self.bitcoin_rpc_url.or(source.bitcoin_rpc_url),
114      bitcoin_rpc_username: self.bitcoin_rpc_username.or(source.bitcoin_rpc_username),
115      postgres_uri: self.postgres_uri.or(source.postgres_uri),
116      chain: self.chain.or(source.chain),
117      commit_interval: self.commit_interval.or(source.commit_interval),
118      config: self.config.or(source.config),
119      config_dir: self.config_dir.or(source.config_dir),
120      cookie_file: self.cookie_file.or(source.cookie_file),
121      data_dir: self.data_dir.or(source.data_dir),
122      first_inscription_height: self
123        .first_inscription_height
124        .or(source.first_inscription_height),
125      height_limit: self.height_limit.or(source.height_limit),
126      hidden: Some(
127        self
128          .hidden
129          .iter()
130          .flatten()
131          .chain(source.hidden.iter().flatten())
132          .cloned()
133          .collect(),
134      ),
135      index: self.index.or(source.index),
136      index_cache_size: self.index_cache_size.or(source.index_cache_size),
137      index_runes: self.index_runes || source.index_runes,
138      index_sats: self.index_sats || source.index_sats,
139      index_spent_sats: self.index_spent_sats || source.index_spent_sats,
140      index_transactions: self.index_transactions || source.index_transactions,
141      integration_test: self.integration_test || source.integration_test,
142      no_index_inscriptions: self.no_index_inscriptions || source.no_index_inscriptions,
143      server_password: self.server_password.or(source.server_password),
144      server_url: self.server_url.or(source.server_url),
145      server_username: self.server_username.or(source.server_username),
146    }
147  }
148
149  pub(crate) fn from_options(options: Options) -> Self {
150    Self {
151      bitcoin_data_dir: options.bitcoin_data_dir,
152      bitcoin_rpc_password: options.bitcoin_rpc_password,
153      bitcoin_rpc_url: options.bitcoin_rpc_url,
154      bitcoin_rpc_username: options.bitcoin_rpc_username,
155      postgres_uri: options.postgres_uri,
156      chain: options
157        .signet
158        .then_some(Chain::Signet)
159        .or(options.regtest.then_some(Chain::Regtest))
160        .or(options.testnet.then_some(Chain::Testnet))
161        .or(options.chain_argument),
162      commit_interval: options.commit_interval,
163      config: options.config,
164      config_dir: options.config_dir,
165      cookie_file: options.cookie_file,
166      data_dir: options.data_dir,
167      first_inscription_height: options.first_inscription_height,
168      height_limit: options.height_limit,
169      hidden: None,
170      index: options.index,
171      index_cache_size: options.index_cache_size,
172      index_runes: options.index_runes,
173      index_sats: options.index_sats,
174      index_spent_sats: options.index_spent_sats,
175      index_transactions: options.index_transactions,
176      integration_test: options.integration_test,
177      no_index_inscriptions: options.no_index_inscriptions,
178      server_password: options.server_password,
179      server_url: None,
180      server_username: options.server_username,
181    }
182  }
183
184  pub(crate) fn from_env(env: BTreeMap<String, String>) -> Result<Self> {
185    let get_bool = |key| {
186      env
187        .get(key)
188        .map(|value| !value.is_empty())
189        .unwrap_or_default()
190    };
191
192    let get_string = |key| env.get(key).cloned();
193
194    let get_path = |key| env.get(key).map(PathBuf::from);
195
196    let get_chain = |key| {
197      env
198        .get(key)
199        .map(|chain| chain.parse::<Chain>())
200        .transpose()
201        .with_context(|| format!("failed to parse environment variable ORD_{key} as chain"))
202    };
203
204    let inscriptions = |key| {
205      env
206        .get(key)
207        .map(|inscriptions| {
208          inscriptions
209            .split_whitespace()
210            .map(|inscription_id| inscription_id.parse::<InscriptionId>())
211            .collect::<Result<HashSet<InscriptionId>, inscription_id::ParseError>>()
212        })
213        .transpose()
214        .with_context(|| {
215          format!("failed to parse environment variable ORD_{key} as inscription list")
216        })
217    };
218
219    let get_u32 = |key| {
220      env
221        .get(key)
222        .map(|int| int.parse::<u32>())
223        .transpose()
224        .with_context(|| format!("failed to parse environment variable ORD_{key} as u32"))
225    };
226    let get_usize = |key| {
227      env
228        .get(key)
229        .map(|int| int.parse::<usize>())
230        .transpose()
231        .with_context(|| format!("failed to parse environment variable ORD_{key} as usize"))
232    };
233
234    Ok(Self {
235      bitcoin_data_dir: get_path("BITCOIN_DATA_DIR"),
236      bitcoin_rpc_password: get_string("BITCOIN_RPC_PASSWORD"),
237      bitcoin_rpc_url: get_string("BITCOIN_RPC_URL"),
238      bitcoin_rpc_username: get_string("BITCOIN_RPC_USERNAME"),
239      postgres_uri: get_string("POSTGRES_URI"),
240      chain: get_chain("CHAIN")?,
241      commit_interval: get_usize("COMMIT_INTERVAL")?,
242      config: get_path("CONFIG"),
243      config_dir: get_path("CONFIG_DIR"),
244      cookie_file: get_path("COOKIE_FILE"),
245      data_dir: get_path("DATA_DIR"),
246      first_inscription_height: get_u32("FIRST_INSCRIPTION_HEIGHT")?,
247      height_limit: get_u32("HEIGHT_LIMIT")?,
248      hidden: inscriptions("HIDDEN")?,
249      index: get_path("INDEX"),
250      index_cache_size: get_usize("INDEX_CACHE_SIZE")?,
251      index_runes: get_bool("INDEX_RUNES"),
252      index_sats: get_bool("INDEX_SATS"),
253      index_spent_sats: get_bool("INDEX_SPENT_SATS"),
254      index_transactions: get_bool("INDEX_TRANSACTIONS"),
255      integration_test: get_bool("INTEGRATION_TEST"),
256      no_index_inscriptions: get_bool("NO_INDEX_INSCRIPTIONS"),
257      server_password: get_string("SERVER_PASSWORD"),
258      server_url: get_string("SERVER_URL"),
259      server_username: get_string("SERVER_USERNAME"),
260    })
261  }
262
263  pub(crate) fn for_env(dir: &Path, rpc_url: &str, server_url: &str) -> Self {
264    Self {
265      bitcoin_data_dir: Some(dir.into()),
266      bitcoin_rpc_password: None,
267      bitcoin_rpc_url: Some(rpc_url.into()),
268      bitcoin_rpc_username: None,
269      postgres_uri: None,
270      chain: Some(Chain::Regtest),
271      commit_interval: None,
272      config: None,
273      config_dir: None,
274      cookie_file: None,
275      data_dir: Some(dir.into()),
276      first_inscription_height: None,
277      height_limit: None,
278      hidden: None,
279      index: None,
280      index_cache_size: None,
281      index_runes: true,
282      index_sats: true,
283      index_spent_sats: false,
284      index_transactions: false,
285      integration_test: false,
286      no_index_inscriptions: false,
287      server_password: None,
288      server_url: Some(server_url.into()),
289      server_username: None,
290    }
291  }
292
293  pub fn or_defaults(self) -> Result<Self> {
294    let chain = self.chain.unwrap_or_default();
295
296    let bitcoin_data_dir = match &self.bitcoin_data_dir {
297      Some(bitcoin_data_dir) => bitcoin_data_dir.clone(),
298      None => {
299        if cfg!(target_os = "linux") {
300          dirs::home_dir()
301            .ok_or_else(|| anyhow!("failed to get cookie file path: could not get home dir"))?
302            .join(".bitcoin")
303        } else {
304          dirs::data_dir()
305            .ok_or_else(|| anyhow!("failed to get cookie file path: could not get data dir"))?
306            .join("Bitcoin")
307        }
308      }
309    };
310
311    let cookie_file = match self.cookie_file {
312      Some(cookie_file) => cookie_file,
313      None => chain.join_with_data_dir(&bitcoin_data_dir).join(".cookie"),
314    };
315
316    let data_dir = chain.join_with_data_dir(match &self.data_dir {
317      Some(data_dir) => data_dir.clone(),
318      None => Self::default_data_dir()?,
319    });
320
321    let index = match &self.index {
322      Some(path) => path.clone(),
323      None => data_dir.join("index.redb"),
324    };
325
326    Ok(Self {
327      bitcoin_data_dir: Some(bitcoin_data_dir),
328      bitcoin_rpc_password: self.bitcoin_rpc_password,
329      bitcoin_rpc_url: Some(
330        self
331          .bitcoin_rpc_url
332          .clone()
333          .unwrap_or_else(|| format!("127.0.0.1:{}", chain.default_rpc_port())),
334      ),
335      bitcoin_rpc_username: self.bitcoin_rpc_username,
336      postgres_uri: self.postgres_uri,
337      chain: Some(chain),
338      commit_interval: Some(self.commit_interval.unwrap_or(5000)),
339      config: None,
340      config_dir: None,
341      cookie_file: Some(cookie_file),
342      data_dir: Some(data_dir),
343      first_inscription_height: Some(if self.integration_test {
344        0
345      } else {
346        self
347          .first_inscription_height
348          .unwrap_or_else(|| chain.first_inscription_height())
349      }),
350      height_limit: self.height_limit,
351      hidden: self.hidden,
352      index: Some(index),
353      index_cache_size: Some(match self.index_cache_size {
354        Some(index_cache_size) => index_cache_size,
355        None => {
356          let mut sys = System::new();
357          sys.refresh_memory();
358          usize::try_from(sys.total_memory() / 4)?
359        }
360      }),
361      index_runes: self.index_runes,
362      index_sats: self.index_sats,
363      index_spent_sats: self.index_spent_sats,
364      index_transactions: self.index_transactions,
365      integration_test: self.integration_test,
366      no_index_inscriptions: self.no_index_inscriptions,
367      server_password: self.server_password,
368      server_url: self.server_url,
369      server_username: self.server_username,
370    })
371  }
372
373  pub(crate) fn default_data_dir() -> Result<PathBuf> {
374    Ok(
375      dirs::data_dir()
376        .context("could not get data dir")?
377        .join("ord"),
378    )
379  }
380
381  pub(crate) fn bitcoin_credentials(&self) -> Result<Auth> {
382    if let Some((user, pass)) = &self
383      .bitcoin_rpc_username
384      .as_ref()
385      .zip(self.bitcoin_rpc_password.as_ref())
386    {
387      Ok(Auth::UserPass((*user).clone(), (*pass).clone()))
388    } else {
389      Ok(Auth::CookieFile(self.cookie_file()?))
390    }
391  }
392
393  pub fn bitcoin_rpc_client(&self, wallet: Option<String>) -> Result<Client> {
394    let rpc_url = self.bitcoin_rpc_url(wallet);
395
396    let bitcoin_credentials = self.bitcoin_credentials()?;
397
398    log::info!(
399      "Connecting to Bitcoin Core at {}",
400      self.bitcoin_rpc_url(None)
401    );
402
403    if let Auth::CookieFile(cookie_file) = &bitcoin_credentials {
404      log::info!(
405        "Using credentials from cookie file at `{}`",
406        cookie_file.display()
407      );
408
409      ensure!(
410        cookie_file.is_file(),
411        "cookie file `{}` does not exist",
412        cookie_file.display()
413      );
414    }
415
416    let client = Client::new(&rpc_url, bitcoin_credentials)
417      .with_context(|| format!("failed to connect to Bitcoin Core RPC at `{rpc_url}`"))?;
418
419    let mut checks = 0;
420    let rpc_chain = loop {
421      match client.get_blockchain_info() {
422        Ok(blockchain_info) => {
423          break match blockchain_info.chain.as_str() {
424            "main" => Chain::Mainnet,
425            "test" => Chain::Testnet,
426            "regtest" => Chain::Regtest,
427            "signet" => Chain::Signet,
428            other => bail!("Bitcoin RPC server on unknown chain: {other}"),
429          }
430        }
431        Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err)))
432          if err.code == -28 => {}
433        Err(err) => bail!("Failed to connect to Bitcoin Core RPC at `{rpc_url}`:  {err}"),
434      }
435
436      ensure! {
437        checks < 100,
438        "Failed to connect to Bitcoin Core RPC at `{rpc_url}`",
439      }
440
441      checks += 1;
442      thread::sleep(Duration::from_millis(100));
443    };
444
445    let ord_chain = self.chain();
446
447    if rpc_chain != ord_chain {
448      bail!("Bitcoin RPC server is on {rpc_chain} but ord is on {ord_chain}");
449    }
450
451    Ok(client)
452  }
453
454  pub fn chain(&self) -> Chain {
455    self.chain.unwrap()
456  }
457
458  pub(crate) fn commit_interval(&self) -> usize {
459    self.commit_interval.unwrap()
460  }
461
462  pub(crate) fn cookie_file(&self) -> Result<PathBuf> {
463    if let Some(cookie_file) = &self.cookie_file {
464      return Ok(cookie_file.clone());
465    }
466
467    let path = if let Some(bitcoin_data_dir) = &self.bitcoin_data_dir {
468      bitcoin_data_dir.clone()
469    } else if cfg!(target_os = "linux") {
470      dirs::home_dir()
471        .ok_or_else(|| anyhow!("failed to get cookie file path: could not get home dir"))?
472        .join(".bitcoin")
473    } else {
474      dirs::data_dir()
475        .ok_or_else(|| anyhow!("failed to get cookie file path: could not get data dir"))?
476        .join("Bitcoin")
477    };
478
479    let path = self.chain().join_with_data_dir(path);
480
481    Ok(path.join(".cookie"))
482  }
483
484  pub(crate) fn credentials(&self) -> Option<(&str, &str)> {
485    self
486      .server_username
487      .as_deref()
488      .zip(self.server_password.as_deref())
489  }
490
491  pub(crate) fn data_dir(&self) -> PathBuf {
492    self.data_dir.as_ref().unwrap().into()
493  }
494
495  pub(crate) fn first_inscription_height(&self) -> u32 {
496    self.first_inscription_height.unwrap()
497  }
498
499  pub(crate) fn first_rune_height(&self) -> u32 {
500    if self.integration_test {
501      0
502    } else {
503      self.chain.unwrap().first_rune_height()
504    }
505  }
506
507  pub(crate) fn height_limit(&self) -> Option<u32> {
508    self.height_limit
509  }
510
511  pub(crate) fn index(&self) -> &Path {
512    self.index.as_ref().unwrap()
513  }
514
515  pub(crate) fn index_inscriptions(&self) -> bool {
516    !self.no_index_inscriptions
517  }
518
519  pub(crate) fn index_runes(&self) -> bool {
520    self.index_runes
521  }
522
523  pub(crate) fn index_cache_size(&self) -> usize {
524    self.index_cache_size.unwrap()
525  }
526
527  pub(crate) fn index_sats(&self) -> bool {
528    self.index_sats
529  }
530
531  pub(crate) fn index_spent_sats(&self) -> bool {
532    self.index_spent_sats
533  }
534
535  pub(crate) fn index_transactions(&self) -> bool {
536    self.index_transactions
537  }
538
539  pub(crate) fn integration_test(&self) -> bool {
540    self.integration_test
541  }
542
543  pub(crate) fn is_hidden(&self, inscription_id: InscriptionId) -> bool {
544    self
545      .hidden
546      .as_ref()
547      .map(|hidden| hidden.contains(&inscription_id))
548      .unwrap_or_default()
549  }
550
551  pub(crate) fn bitcoin_rpc_url(&self, wallet_name: Option<String>) -> String {
552    let base_url = self.bitcoin_rpc_url.as_ref().unwrap();
553    match wallet_name {
554      Some(wallet_name) => format!("{base_url}/wallet/{wallet_name}"),
555      None => format!("{base_url}/"),
556    }
557  }
558
559  pub(crate) fn server_url(&self) -> Option<&str> {
560    self.server_url.as_deref()
561  }
562}
563
564#[cfg(test)]
565mod tests {
566  use super::*;
567
568  fn parse(args: &[&str]) -> Settings {
569    let args = iter::once("ord")
570      .chain(args.iter().copied())
571      .collect::<Vec<&str>>();
572    Settings::from_options(Options::try_parse_from(args).unwrap())
573      .or_defaults()
574      .unwrap()
575  }
576
577  fn wallet(args: &str) -> (Settings, subcommand::wallet::WalletCommand) {
578    match Arguments::try_parse_from(args.split_whitespace()) {
579      Ok(arguments) => match arguments.subcommand {
580        Subcommand::Wallet(wallet) => (
581          Settings::from_options(arguments.options)
582            .or_defaults()
583            .unwrap(),
584          wallet,
585        ),
586        subcommand => panic!("unexpected subcommand: {subcommand:?}"),
587      },
588      Err(err) => panic!("error parsing arguments: {err}"),
589    }
590  }
591
592  #[test]
593  fn auth_missing_rpc_pass_is_an_error() {
594    assert_eq!(
595      Settings::merge(
596        Options {
597          bitcoin_rpc_username: Some("foo".into()),
598          ..default()
599        },
600        Default::default(),
601      )
602      .unwrap_err()
603      .to_string(),
604      "no bitcoin RPC password specified"
605    );
606  }
607
608  #[test]
609  fn auth_missing_rpc_user_is_an_error() {
610    assert_eq!(
611      Settings::merge(
612        Options {
613          bitcoin_rpc_password: Some("foo".into()),
614          ..default()
615        },
616        Default::default(),
617      )
618      .unwrap_err()
619      .to_string(),
620      "no bitcoin RPC username specified"
621    );
622  }
623
624  #[test]
625  fn auth_with_user_and_pass() {
626    assert_eq!(
627      parse(&["--bitcoin-rpc-username=foo", "--bitcoin-rpc-password=bar"])
628        .bitcoin_credentials()
629        .unwrap(),
630      Auth::UserPass("foo".into(), "bar".into())
631    );
632  }
633
634  #[test]
635  fn auth_with_cookie_file() {
636    assert_eq!(
637      parse(&["--cookie-file=/var/lib/Bitcoin/.cookie"])
638        .bitcoin_credentials()
639        .unwrap(),
640      Auth::CookieFile("/var/lib/Bitcoin/.cookie".into())
641    );
642  }
643
644  #[test]
645  fn cookie_file_does_not_exist_error() {
646    assert_eq!(
647      parse(&["--cookie-file=/foo/bar/baz/qux/.cookie"])
648        .bitcoin_rpc_client(None)
649        .err()
650        .unwrap()
651        .to_string(),
652      "cookie file `/foo/bar/baz/qux/.cookie` does not exist"
653    );
654  }
655
656  #[test]
657  fn rpc_server_chain_must_match() {
658    let core = mockcore::builder().network(Network::Testnet).build();
659
660    let settings = parse(&[
661      "--cookie-file",
662      core.cookie_file().to_str().unwrap(),
663      "--bitcoin-rpc-url",
664      &core.url(),
665    ]);
666
667    assert_eq!(
668      settings.bitcoin_rpc_client(None).unwrap_err().to_string(),
669      "Bitcoin RPC server is on testnet but ord is on mainnet"
670    );
671  }
672
673  #[test]
674  fn rpc_url_overrides_network() {
675    assert_eq!(
676      parse(&["--bitcoin-rpc-url=127.0.0.1:1234", "--chain=signet"]).bitcoin_rpc_url(None),
677      "127.0.0.1:1234/"
678    );
679  }
680
681  #[test]
682  fn cookie_file_overrides_network() {
683    assert_eq!(
684      parse(&["--cookie-file=/foo/bar", "--chain=signet"])
685        .cookie_file()
686        .unwrap(),
687      Path::new("/foo/bar")
688    );
689  }
690
691  #[test]
692  fn use_default_network() {
693    let settings = parse(&[]);
694
695    assert_eq!(settings.bitcoin_rpc_url(None), "127.0.0.1:8332/");
696
697    assert!(settings.cookie_file().unwrap().ends_with(".cookie"));
698  }
699
700  #[test]
701  fn uses_network_defaults() {
702    let settings = parse(&["--chain=signet"]);
703
704    assert_eq!(settings.bitcoin_rpc_url(None), "127.0.0.1:38332/");
705
706    assert!(settings
707      .cookie_file()
708      .unwrap()
709      .display()
710      .to_string()
711      .ends_with(if cfg!(windows) {
712        r"\signet\.cookie"
713      } else {
714        "/signet/.cookie"
715      }));
716  }
717
718  #[test]
719  fn mainnet_cookie_file_path() {
720    let cookie_file = parse(&[]).cookie_file().unwrap().display().to_string();
721
722    assert!(cookie_file.ends_with(if cfg!(target_os = "linux") {
723      "/.bitcoin/.cookie"
724    } else if cfg!(windows) {
725      r"\Bitcoin\.cookie"
726    } else {
727      "/Bitcoin/.cookie"
728    }))
729  }
730
731  #[test]
732  fn othernet_cookie_file_path() {
733    let cookie_file = parse(&["--chain=signet"])
734      .cookie_file()
735      .unwrap()
736      .display()
737      .to_string();
738
739    assert!(cookie_file.ends_with(if cfg!(target_os = "linux") {
740      "/.bitcoin/signet/.cookie"
741    } else if cfg!(windows) {
742      r"\Bitcoin\signet\.cookie"
743    } else {
744      "/Bitcoin/signet/.cookie"
745    }));
746  }
747
748  #[test]
749  fn cookie_file_defaults_to_bitcoin_data_dir() {
750    let cookie_file = parse(&["--bitcoin-data-dir=foo", "--chain=signet"])
751      .cookie_file()
752      .unwrap()
753      .display()
754      .to_string();
755
756    assert!(cookie_file.ends_with(if cfg!(windows) {
757      r"foo\signet\.cookie"
758    } else {
759      "foo/signet/.cookie"
760    }));
761  }
762
763  #[test]
764  fn mainnet_data_dir() {
765    let data_dir = parse(&[]).data_dir().display().to_string();
766    assert!(
767      data_dir.ends_with(if cfg!(windows) { r"\ord" } else { "/ord" }),
768      "{data_dir}"
769    );
770  }
771
772  #[test]
773  fn othernet_data_dir() {
774    let data_dir = parse(&["--chain=signet"]).data_dir().display().to_string();
775    assert!(
776      data_dir.ends_with(if cfg!(windows) {
777        r"\ord\signet"
778      } else {
779        "/ord/signet"
780      }),
781      "{data_dir}"
782    );
783  }
784
785  #[test]
786  fn network_is_joined_with_data_dir() {
787    let data_dir = parse(&["--chain=signet", "--datadir=foo"])
788      .data_dir()
789      .display()
790      .to_string();
791    assert!(
792      data_dir.ends_with(if cfg!(windows) {
793        r"foo\signet"
794      } else {
795        "foo/signet"
796      }),
797      "{data_dir}"
798    );
799  }
800
801  #[test]
802  fn network_accepts_aliases() {
803    fn check_network_alias(alias: &str, suffix: &str) {
804      let data_dir = parse(&["--chain", alias]).data_dir().display().to_string();
805
806      assert!(data_dir.ends_with(suffix), "{data_dir}");
807    }
808
809    check_network_alias("main", "ord");
810    check_network_alias("mainnet", "ord");
811    check_network_alias(
812      "regtest",
813      if cfg!(windows) {
814        r"ord\regtest"
815      } else {
816        "ord/regtest"
817      },
818    );
819    check_network_alias(
820      "signet",
821      if cfg!(windows) {
822        r"ord\signet"
823      } else {
824        "ord/signet"
825      },
826    );
827    check_network_alias(
828      "test",
829      if cfg!(windows) {
830        r"ord\testnet3"
831      } else {
832        "ord/testnet3"
833      },
834    );
835    check_network_alias(
836      "testnet",
837      if cfg!(windows) {
838        r"ord\testnet3"
839      } else {
840        "ord/testnet3"
841      },
842    );
843  }
844
845  #[test]
846  fn chain_flags() {
847    Arguments::try_parse_from(["ord", "--signet", "--chain", "signet", "index", "update"])
848      .unwrap_err();
849    assert_eq!(parse(&["--signet"]).chain(), Chain::Signet);
850    assert_eq!(parse(&["-s"]).chain(), Chain::Signet);
851
852    Arguments::try_parse_from(["ord", "--regtest", "--chain", "signet", "index", "update"])
853      .unwrap_err();
854    assert_eq!(parse(&["--regtest"]).chain(), Chain::Regtest);
855    assert_eq!(parse(&["-r"]).chain(), Chain::Regtest);
856
857    Arguments::try_parse_from(["ord", "--testnet", "--chain", "signet", "index", "update"])
858      .unwrap_err();
859    assert_eq!(parse(&["--testnet"]).chain(), Chain::Testnet);
860    assert_eq!(parse(&["-t"]).chain(), Chain::Testnet);
861  }
862
863  #[test]
864  fn wallet_flag_overrides_default_name() {
865    assert_eq!(wallet("ord wallet create").1.name, "ord");
866    assert_eq!(wallet("ord wallet --name foo create").1.name, "foo")
867  }
868
869  #[test]
870  fn uses_wallet_rpc() {
871    let (settings, _) = wallet("ord wallet --name foo balance");
872
873    assert_eq!(
874      settings.bitcoin_rpc_url(Some("foo".into())),
875      "127.0.0.1:8332/wallet/foo"
876    );
877  }
878
879  #[test]
880  fn setting_index_cache_size() {
881    assert_eq!(
882      parse(&["--index-cache-size=16000000000",]).index_cache_size(),
883      16000000000
884    );
885  }
886
887  #[test]
888  fn setting_commit_interval() {
889    let arguments =
890      Arguments::try_parse_from(["ord", "--commit-interval", "500", "index", "update"]).unwrap();
891    assert_eq!(arguments.options.commit_interval, Some(500));
892  }
893
894  #[test]
895  fn index_runes() {
896    assert!(parse(&["--chain=signet", "--index-runes"]).index_runes());
897    assert!(parse(&["--index-runes"]).index_runes());
898    assert!(!parse(&[]).index_runes());
899  }
900
901  #[test]
902  fn bitcoin_rpc_and_pass_setting() {
903    let config = Settings {
904      bitcoin_rpc_username: Some("config_user".into()),
905      bitcoin_rpc_password: Some("config_pass".into()),
906      ..default()
907    };
908
909    let tempdir = TempDir::new().unwrap();
910
911    let config_path = tempdir.path().join("ord.yaml");
912
913    fs::write(&config_path, serde_yaml::to_string(&config).unwrap()).unwrap();
914
915    assert_eq!(
916      Settings::merge(
917        Options {
918          bitcoin_rpc_username: Some("option_user".into()),
919          bitcoin_rpc_password: Some("option_pass".into()),
920          config: Some(config_path.clone()),
921          ..default()
922        },
923        vec![
924          ("BITCOIN_RPC_USERNAME".into(), "env_user".into()),
925          ("BITCOIN_RPC_PASSWORD".into(), "env_pass".into()),
926        ]
927        .into_iter()
928        .collect(),
929      )
930      .unwrap()
931      .bitcoin_credentials()
932      .unwrap(),
933      Auth::UserPass("option_user".into(), "option_pass".into()),
934    );
935
936    assert_eq!(
937      Settings::merge(
938        Options {
939          config: Some(config_path.clone()),
940          ..default()
941        },
942        vec![
943          ("BITCOIN_RPC_USERNAME".into(), "env_user".into()),
944          ("BITCOIN_RPC_PASSWORD".into(), "env_pass".into()),
945        ]
946        .into_iter()
947        .collect(),
948      )
949      .unwrap()
950      .bitcoin_credentials()
951      .unwrap(),
952      Auth::UserPass("env_user".into(), "env_pass".into()),
953    );
954
955    assert_eq!(
956      Settings::merge(
957        Options {
958          config: Some(config_path),
959          ..default()
960        },
961        Default::default(),
962      )
963      .unwrap()
964      .bitcoin_credentials()
965      .unwrap(),
966      Auth::UserPass("config_user".into(), "config_pass".into()),
967    );
968
969    assert_matches!(
970      Settings::merge(Default::default(), Default::default())
971        .unwrap()
972        .bitcoin_credentials()
973        .unwrap(),
974      Auth::CookieFile(_),
975    );
976  }
977
978  #[test]
979  fn example_config_file_is_valid() {
980    let _: Settings = serde_yaml::from_reader(fs::File::open("ord.yaml").unwrap()).unwrap();
981  }
982
983  #[test]
984  fn from_env() {
985    let env = vec![
986      ("BITCOIN_DATA_DIR", "/bitcoin/data/dir"),
987      ("BITCOIN_RPC_PASSWORD", "bitcoin password"),
988      ("BITCOIN_RPC_URL", "url"),
989      ("BITCOIN_RPC_USERNAME", "bitcoin username"),
990      ("CHAIN", "signet"),
991      ("COMMIT_INTERVAL", "1"),
992      ("CONFIG", "config"),
993      ("CONFIG_DIR", "config dir"),
994      ("COOKIE_FILE", "cookie file"),
995      ("DATA_DIR", "/data/dir"),
996      ("FIRST_INSCRIPTION_HEIGHT", "2"),
997      ("HEIGHT_LIMIT", "3"),
998      ("HIDDEN", "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0 703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0"),
999      ("INDEX", "index"),
1000      ("INDEX_CACHE_SIZE", "4"),
1001      ("INDEX_RUNES", "1"),
1002      ("INDEX_SATS", "1"),
1003      ("INDEX_SPENT_SATS", "1"),
1004      ("INDEX_TRANSACTIONS", "1"),
1005      ("INTEGRATION_TEST", "1"),
1006      ("NO_INDEX_INSCRIPTIONS", "1"),
1007      ("SERVER_PASSWORD", "server password"),
1008      ("SERVER_URL", "server url"),
1009      ("SERVER_USERNAME", "server username"),
1010    ]
1011    .into_iter()
1012    .map(|(key, value)| (key.into(), value.into()))
1013    .collect::<BTreeMap<String, String>>();
1014
1015    pretty_assert_eq!(
1016      Settings::from_env(env).unwrap(),
1017      Settings {
1018        bitcoin_data_dir: Some("/bitcoin/data/dir".into()),
1019        bitcoin_rpc_password: Some("bitcoin password".into()),
1020        bitcoin_rpc_url: Some("url".into()),
1021        bitcoin_rpc_username: Some("bitcoin username".into()),
1022        postgres_uri: Some("postgres://btc_indexer:btc@localhost/btc_indexer".into()),
1023        chain: Some(Chain::Signet),
1024        commit_interval: Some(1),
1025        config: Some("config".into()),
1026        config_dir: Some("config dir".into()),
1027        cookie_file: Some("cookie file".into()),
1028        data_dir: Some("/data/dir".into()),
1029        first_inscription_height: Some(2),
1030        height_limit: Some(3),
1031        hidden: Some(
1032          vec![
1033            "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"
1034              .parse()
1035              .unwrap(),
1036            "703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0"
1037              .parse()
1038              .unwrap()
1039          ]
1040          .into_iter()
1041          .collect()
1042        ),
1043        index: Some("index".into()),
1044        index_cache_size: Some(4),
1045        index_runes: true,
1046        index_sats: true,
1047        index_spent_sats: true,
1048        index_transactions: true,
1049        integration_test: true,
1050        no_index_inscriptions: true,
1051        server_password: Some("server password".into()),
1052        server_url: Some("server url".into()),
1053        server_username: Some("server username".into()),
1054      }
1055    );
1056  }
1057
1058  #[test]
1059  fn from_options() {
1060    pretty_assert_eq!(
1061      Settings::from_options(
1062        Options::try_parse_from([
1063          "ord",
1064          "--bitcoin-data-dir=/bitcoin/data/dir",
1065          "--bitcoin-rpc-password=bitcoin password",
1066          "--bitcoin-rpc-url=url",
1067          "--bitcoin-rpc-username=bitcoin username",
1068          "--postgres_uri=postgresql db uri",
1069          "--chain=signet",
1070          "--commit-interval=1",
1071          "--config=config",
1072          "--config-dir=config dir",
1073          "--cookie-file=cookie file",
1074          "--datadir=/data/dir",
1075          "--first-inscription-height=2",
1076          "--height-limit=3",
1077          "--index-cache-size=4",
1078          "--index-runes",
1079          "--index-sats",
1080          "--index-spent-sats",
1081          "--index-transactions",
1082          "--index=index",
1083          "--integration-test",
1084          "--no-index-inscriptions",
1085          "--server-password=server password",
1086          "--server-username=server username",
1087        ])
1088        .unwrap()
1089      ),
1090      Settings {
1091        bitcoin_data_dir: Some("/bitcoin/data/dir".into()),
1092        bitcoin_rpc_password: Some("bitcoin password".into()),
1093        bitcoin_rpc_url: Some("url".into()),
1094        bitcoin_rpc_username: Some("bitcoin username".into()),
1095        postgres_uri: Some("postgres://btc_indexer:btc@localhost/btc_indexer".into()),
1096        chain: Some(Chain::Signet),
1097        commit_interval: Some(1),
1098        config: Some("config".into()),
1099        config_dir: Some("config dir".into()),
1100        cookie_file: Some("cookie file".into()),
1101        data_dir: Some("/data/dir".into()),
1102        first_inscription_height: Some(2),
1103        height_limit: Some(3),
1104        hidden: None,
1105        index: Some("index".into()),
1106        index_cache_size: Some(4),
1107        index_runes: true,
1108        index_sats: true,
1109        index_spent_sats: true,
1110        index_transactions: true,
1111        integration_test: true,
1112        no_index_inscriptions: true,
1113        server_password: Some("server password".into()),
1114        server_url: None,
1115        server_username: Some("server username".into()),
1116      }
1117    );
1118  }
1119
1120  #[test]
1121  fn merge() {
1122    let env = vec![("INDEX", "env")]
1123      .into_iter()
1124      .map(|(key, value)| (key.into(), value.into()))
1125      .collect::<BTreeMap<String, String>>();
1126
1127    let config = Settings {
1128      index: Some("config".into()),
1129      ..default()
1130    };
1131
1132    let tempdir = TempDir::new().unwrap();
1133
1134    let config_path = tempdir.path().join("ord.yaml");
1135
1136    fs::write(&config_path, serde_yaml::to_string(&config).unwrap()).unwrap();
1137
1138    let options =
1139      Options::try_parse_from(["ord", "--config", config_path.to_str().unwrap()]).unwrap();
1140
1141    pretty_assert_eq!(
1142      Settings::merge(options.clone(), Default::default())
1143        .unwrap()
1144        .index,
1145      Some("config".into()),
1146    );
1147
1148    pretty_assert_eq!(
1149      Settings::merge(options, env.clone()).unwrap().index,
1150      Some("env".into()),
1151    );
1152
1153    let options = Options::try_parse_from([
1154      "ord",
1155      "--index=option",
1156      "--config",
1157      config_path.to_str().unwrap(),
1158    ])
1159    .unwrap();
1160
1161    pretty_assert_eq!(
1162      Settings::merge(options, env).unwrap().index,
1163      Some("option".into()),
1164    );
1165  }
1166}
1167