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>, 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