1use std::fs;
32use std::path::Path;
33use std::sync::Arc;
34use std::str::FromStr;
35
36use anyhow::{Context, bail};
37use bark::chain::ChainSource;
38use bark::fs_perms;
39use bark::persist::adaptor::StorageAdaptorWrapper;
40use bitcoin::Network;
41use clap::Args;
42use log::{debug, info, warn};
43use tonic::transport::Uri;
44
45use bark::{BarkNetwork, Config, OpenWalletArgs, Wallet as BarkWallet, WalletSeed};
46use bark::lock_manager::{self, LockManager, PidLockError};
47use bark::lock_manager::pid_flock::LOCK_FILE;
48use bark::onchain::OnchainWallet;
49use bark::persist::BarkPersister;
50use bark::persist::sqlite::{SqliteClient, DEFAULT_DB_FILE};
51use bark::persist::adaptor::filestore::FileStorageAdaptor;
52
53use bitcoin_ext::BlockHeight;
54
55use crate::connection::{self, BARKD_LOCK_FILE};
56use crate::util;
57
58const MNEMONIC_FILE: &str = "mnemonic";
60
61const DB_FILE: &str = DEFAULT_DB_FILE;
63const FILESTORE_FILE: &str = "wallet.json";
65
66const CONFIG_FILE: &str = "config.toml";
68
69const DEBUG_LOG_FILE: &str = "debug.log";
71
72pub const AUTH_TOKEN_FILE: &str = "auth_token";
74
75const STDOUT_LOG_FILE: &str = "stdout.log";
78const STDERR_LOG_FILE: &str = "stderr.log";
79
80fn open_lock_manager(datadir: &Path) -> anyhow::Result<Box<dyn LockManager>> {
83 match lock_manager::platform_default(Some(datadir), None) {
84 Ok(m) => Ok(m),
85 Err(e) if e.is::<PidLockError>() => Err(e),
86 Err(e) => Err(e.context("failed to acquire datadir lock")),
87 }
88}
89
90#[derive(Clone, PartialEq, Eq, Default, clap::Args)]
92pub struct ConfigOpts {
93 #[arg(long)]
95 pub ark: Option<String>,
96
97 #[arg(long, hide = true)]
100 pub access_token: Option<String>,
101
102 #[arg(long)]
106 pub esplora: Option<String>,
107
108 #[arg(long)]
112 pub bitcoind: Option<String>,
113
114 #[arg(long)]
118 pub bitcoind_cookie: Option<String>,
119
120 #[arg(long)]
124 pub bitcoind_user: Option<String>,
125
126 #[arg(long)]
130 pub bitcoind_pass: Option<String>,
131
132 #[arg(long)]
135 pub socks5_proxy: Option<String>,
136
137 #[arg(long)]
140 pub gap_limit: Option<u32>,
141}
142
143impl ConfigOpts {
144 pub fn fill_network_defaults(&mut self, net: BarkNetwork) {
146 if net == BarkNetwork::Mainnet {
148 if self.esplora.is_none() && self.bitcoind.is_none() {
150 self.esplora = Some("https://mempool.second.tech/api".to_owned());
151 }
152
153 if self.ark.is_none() {
154 self.ark = Some("https://ark.second.tech/".to_owned());
155 }
156 }
157
158 if net == BarkNetwork::Signet {
160 if self.esplora.is_none() && self.bitcoind.is_none() {
162 self.esplora = Some("https://esplora.signet.2nd.dev/".to_owned());
163 }
164
165 if self.ark.is_none() {
166 self.ark = Some("https://ark.signet.2nd.dev/".to_owned());
167 }
168 }
169
170 if net == BarkNetwork::Mutinynet && self.esplora.is_none() && self.bitcoind.is_none() {
173 self.esplora = Some("https://mutinynet.com/api".to_owned());
174 }
175 }
176
177 fn validate(&self) -> anyhow::Result<()> {
179 if self.esplora.is_none() && self.bitcoind.is_none() {
180 bail!("You need to provide a chain source using either --esplora or --bitcoind");
181 }
182
183 match (
184 self.bitcoind.is_some(),
185 self.bitcoind_cookie.is_some(),
186 self.bitcoind_user.is_some(),
187 self.bitcoind_pass.is_some(),
188 ) {
189 (false, false, false, false) => {},
190 (false, _, _, _) => bail!("Provided bitcoind auth args without bitcoind address"),
191 (_, true, false, false) => {},
192 (_, true, _, _) => bail!("Bitcoind user/pass shouldn't be provided together with cookie file"),
193 (_, _, true, true) => {},
194 _ => bail!("When providing --bitcoind, you need to provide auth args as well."),
195 }
196
197 if let Some(gap_limit) = self.gap_limit {
198 if gap_limit > bark::MAX_VTXO_KEY_GAP_LIMIT {
199 bail!("--gap-limit {} is above the maximum of {}",
200 gap_limit, bark::MAX_VTXO_KEY_GAP_LIMIT);
201 }
202 }
203
204 if let Some(ref proxy) = self.socks5_proxy {
205 let uri = proxy.parse::<Uri>().context("invalid socks5 proxy URI")?;
206 let scheme = uri.scheme_str().context("invalid socks5 proxy URI scheme")?;
207 if scheme != "socks5h" {
208 bail!("Only socks5h:// proxies are supported");
209 }
210 }
211
212 Ok(())
213 }
214
215 fn write_to_file(&self, network: Network, path: impl AsRef<Path>) -> anyhow::Result<Config> {
219 use std::fmt::Write;
220
221 let mut conf = String::new();
222 let ark = util::default_scheme("https", self.ark.as_ref().context("missing --ark arg")?)
223 .context("invalid ark server URL")?;
224 writeln!(conf, "server_address = \"{}\"", ark).unwrap();
225
226 if let Some(ref v) = self.access_token {
227 writeln!(conf, "server_access_token = \"{}\"", v).unwrap();
228 }
229 if let Some(ref v) = self.esplora {
230 let url = util::default_scheme("https", v).context("invalid esplora URL")?;
231 writeln!(conf, "esplora_address = \"{}\"", url).unwrap();
232 }
233 if let Some(ref v) = self.bitcoind {
234 let url = util::default_scheme("http", v).context("invalid bitcoind URL")?;
235 writeln!(conf, "bitcoind_address = \"{}\"", url).unwrap();
236 }
237 if let Some(ref v) = self.bitcoind_cookie {
238 writeln!(conf, "bitcoind_cookiefile = \"{}\"", v).unwrap();
239 }
240 if let Some(ref v) = self.bitcoind_user {
241 writeln!(conf, "bitcoind_user = \"{}\"", v).unwrap();
242 }
243 if let Some(ref v) = self.bitcoind_pass {
244 writeln!(conf, "bitcoind_pass = \"{}\"", v).unwrap();
245 }
246 if let Some(ref v) = self.socks5_proxy {
247 writeln!(conf, "socks5_proxy = \"{}\"", v).unwrap();
248 }
249 if let Some(v) = self.gap_limit {
250 writeln!(conf, "vtxo_key_gap_limit = {}", v).unwrap();
251 }
252
253 let path = path.as_ref();
254
255 fs_perms::write_atomic_owner_only(path, conf.as_bytes())?;
256
257 Ok(Config::load(network, path).context("problematic config flags provided")?)
259 }
260}
261
262#[derive(Args)]
263pub struct CreateOpts {
264 #[arg(long)]
267 pub force: bool,
268
269 #[arg(long)]
274 pub use_filestore: bool,
275
276 #[arg(long)]
278 pub mainnet: bool,
279 #[arg(long)]
281 pub regtest: bool,
282 #[arg(long)]
284 pub signet: bool,
285 #[arg(long)]
287 pub mutinynet: bool,
288
289 #[arg(long)]
292 pub mnemonic: Option<bip39::Mnemonic>,
293
294 #[arg(long)]
296 pub birthday_height: Option<BlockHeight>,
297
298 #[command(flatten)]
299 pub config: ConfigOpts,
300}
301
302const EXPECTED_DATADIR_FILES: [&str; 6] = [
304 DEBUG_LOG_FILE,
305 STDOUT_LOG_FILE,
306 STDERR_LOG_FILE,
307 LOCK_FILE,
308 AUTH_TOKEN_FILE,
309 BARKD_LOCK_FILE,
310];
311
312fn is_expected_datadir_file(name: &str) -> bool {
315 EXPECTED_DATADIR_FILES.iter().chain(&[CONFIG_FILE]).any(|expected| {
316 name == *expected || name.strip_prefix('.')
317 .and_then(|s| s.strip_prefix(expected))
318 .and_then(|s| s.strip_prefix(".temp."))
319 .is_some_and(|s| s.ends_with(".tmp"))
320 })
321}
322
323async fn check_clean_datadir(datadir: &Path, clean: bool) -> anyhow::Result<bool> {
328 let mut has_config = false;
329 if datadir.exists() {
330 for item in datadir.read_dir().context("error accessing datadir")? {
331 let item = item.context("error reading existing content of datadir")?;
332
333 if item.file_name() == CONFIG_FILE {
334 has_config = true;
335 continue;
336 }
337 if is_expected_datadir_file(&item.file_name().to_string_lossy()) {
338 continue;
339 }
340
341 if !clean {
342 bail!("Datadir has unexpected contents: {}", item.path().display());
343 }
344
345 let file_type = item.file_type().context("error accessing datadir content")?;
347 if file_type.is_dir() {
348 tokio::fs::remove_dir_all(item.path()).await.context("error deleting datadir content")?;
349 } else if file_type.is_file() || file_type.is_symlink() {
350 tokio::fs::remove_file(item.path()).await.context("error deleting datadir content")?;
351 } else {
352 bail!("non-existent file type in ");
354 }
355 }
356 }
357 Ok(has_config)
358}
359
360pub async fn create_wallet(
361 datadir: &Path,
362 user_agent: &str,
363 opts: CreateOpts,
364) -> anyhow::Result<()> {
365 debug!("Creating wallet in {}", datadir.display());
366
367 let net = match (opts.mainnet, opts.signet, opts.regtest, opts.mutinynet) {
368 (true, false, false, false) => BarkNetwork::Mainnet,
369 (false, true, false, false) => BarkNetwork::Signet,
370 (false, false, true, false) => BarkNetwork::Regtest,
371 (false, false, false, true ) => BarkNetwork::Mutinynet,
372 _ => bail!("Specify exactly one of --mainnet, --signet, --regtest or --mutinynet"),
373 };
374
375 let config_existed = check_clean_datadir(datadir, opts.force).await?;
377
378 let result = try_create_wallet(datadir, net, user_agent, opts).await;
380 if let Err(e) = result {
381 if config_existed {
382 if let Err(e) = check_clean_datadir(datadir, true).await {
383 warn!("Error cleaning datadir after failure: {:#}", e);
384 }
385 } else {
386 if let Err(e) = connection::wipe_datadir_except_barkd_files(datadir) {
389 warn!("Error cleaning datadir after failure: {:#}", e);
390 }
391 let _ = fs::remove_dir(datadir);
392 }
393
394 bail!("Error while creating wallet: {:#}", e);
395 }
396 Ok(())
397}
398
399async fn try_create_wallet(
401 datadir: &Path,
402 net: BarkNetwork,
403 user_agent: &str,
404 mut opts: CreateOpts,
405) -> anyhow::Result<()> {
406 info!("Creating new bark Wallet at {}", datadir.display());
407
408 tokio::fs::create_dir_all(datadir).await.context("can't create dir")?;
409
410 fs_perms::harden(datadir, 0o700)?;
411
412 let config_path = datadir.join(CONFIG_FILE);
413 let has_config_args = opts.config != ConfigOpts::default();
414 let mut config = match (config_path.exists(), has_config_args) {
415 (true, false) => {
416 Config::load(net.as_bitcoin(), &config_path).with_context(|| format!(
417 "error loading existing config file at {}", config_path.display(),
418 ))?
419 },
420 (false, _) => {
424 opts.config.fill_network_defaults(net);
425 opts.config.validate().context("invalid config options")?;
426 opts.config.write_to_file(net.as_bitcoin(), config_path)?
427 },
428 (true, true) => bail!("Cannot provide an existing config file and config flags"),
429 };
430
431 if config.user_agent.is_none() {
434 config.user_agent = Some(user_agent.to_owned());
435 }
436
437 if opts.mnemonic.is_some() {
439 if opts.birthday_height.is_none() {
440 if config.bitcoind_address.is_some() {
442 bail!("You need to set the --birthday-height field when recovering from mnemonic.");
443 }
444 } else if config.esplora_address.is_some() {
445 warn!("The given --birthday-height will be ignored because you're using Esplora.");
446 }
447 } else {
448 if opts.birthday_height.is_some() {
449 bail!("Can't set --birthday-height if --mnemonic is not set.");
450 }
451 }
452
453 let chain_spec = config.chain_source()
455 .context("failed to create a chain source client with the config")?;
456 let chain = ChainSource::new(chain_spec, net.as_bitcoin(), None, None).await
457 .context("failed to create a chain source client with the config")?;
458 chain.require_version().await.context("chain source version check failed")?;
459
460 let is_new_wallet = opts.mnemonic.is_none();
462 let mnemonic = opts.mnemonic.unwrap_or_else(|| bip39::Mnemonic::generate(12).expect("12 is valid"));
463 let seed = mnemonic.to_seed("");
464
465 fs_perms::create_new_owner_only(
466 &datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes(),
467 )?;
468
469 let db: Arc<dyn BarkPersister + Send + Sync> = if opts.use_filestore {
471 debug!("Using filestore backend");
472 let adaptor = FileStorageAdaptor::open(datadir.join(FILESTORE_FILE)).await?;
473 Arc::new(StorageAdaptorWrapper::new(adaptor))
474 } else {
475 debug!("Using sqlite backend");
476 let db = SqliteClient::open(datadir.join(DB_FILE))?;
477 fs_perms::harden(&datadir.join(DB_FILE), 0o600)?;
478 Arc::new(db)
479 };
480
481 let mut onchain = OnchainWallet::load_or_create(net.as_bitcoin(), seed, db.clone()).await?;
482 let lock_manager = open_lock_manager(&datadir)?;
483
484 let birthday_height = if is_new_wallet {
486 Some(chain.tip().await?)
487 } else {
488 opts.birthday_height
489 };
490 onchain.initial_wallet_scan(&chain, birthday_height).await?;
491
492 BarkWallet::open(
499 net.as_bitcoin(),
500 WalletSeed::new_from_mnemonic(net.as_bitcoin(), &mnemonic),
501 config,
502 OpenWalletArgs {
503 persister: Some(db),
504 lock_manager: Some(lock_manager),
505 create_if_not_exists: true,
506 create_without_server: opts.force,
507 run_daemon: false,
508 ..Default::default()
509 },
510 ).await.context("error creating wallet")?;
511 Ok(())
512}
513
514pub async fn read_mnemonic(datadir: &Path) -> anyhow::Result<String> {
516 let path = datadir.join(MNEMONIC_FILE);
517 let s = tokio::fs::read_to_string(&path).await
518 .with_context(|| format!("failed to read mnemonic file at {}", path.display()))?;
519 Ok(s.trim().to_string())
520}
521
522pub async fn open_wallet(datadir: &Path, user_agent: &str) -> anyhow::Result<Option<BarkWallet>> {
523 debug!("Opening bark wallet in {}", datadir.display());
524
525 let mnemonic_path = datadir.join(MNEMONIC_FILE);
527
528 if !tokio::fs::try_exists(datadir).await? {
529 return Ok(None);
530 }
531
532 if !tokio::fs::try_exists(&mnemonic_path).await? {
533 return Ok(None);
534 }
535
536 fs_perms::warn_if_loose(datadir, 0o700);
538 fs_perms::warn_if_loose(&mnemonic_path, 0o600);
539 fs_perms::warn_if_loose(&datadir.join(CONFIG_FILE), 0o600);
540
541 let mnemonic_str = tokio::fs::read_to_string(&mnemonic_path).await
542 .with_context(|| format!("failed to read mnemonic file at {}", mnemonic_path.display()))?;
543 let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).context("broken mnemonic")?;
544 let seed = mnemonic.to_seed("");
545
546 let use_filestore = datadir.join(FILESTORE_FILE).exists();
547 let db: Arc<dyn BarkPersister + Send + Sync> = if use_filestore {
548 debug!("Using filestore backend");
549 let adaptor = FileStorageAdaptor::open(datadir.join(FILESTORE_FILE)).await?;
550 Arc::new(StorageAdaptorWrapper::new(adaptor))
551 } else {
552 debug!("Using sqlite backend");
553 Arc::new(SqliteClient::open(datadir.join(DB_FILE))?)
554 };
555 let properties = db.read_properties().await?.context("failed to read properties")?;
556
557 let config_path = datadir.join("config.toml");
559 let mut config = Config::load(properties.network, config_path)
560 .context("error loading bark config file")?;
561
562 if config.user_agent.is_none() {
565 config.user_agent = Some(user_agent.to_owned());
566 }
567
568 let bdk_wallet = OnchainWallet::load_or_create(properties.network, seed, db.clone()).await?;
569 let onchain = Arc::new(tokio::sync::RwLock::new(bdk_wallet));
570 let lock_manager = open_lock_manager(datadir)?;
571 let bark_wallet = BarkWallet::open(
572 properties.network,
573 WalletSeed::new_from_mnemonic(properties.network, &mnemonic),
574 config,
575 OpenWalletArgs {
576 persister: Some(db),
577 lock_manager: Some(lock_manager),
578 onchain: Some(onchain.clone()),
579 run_daemon: false,
580 ..Default::default()
581 },
582 ).await?;
583
584 Ok(Some(bark_wallet))
585}
586
587#[cfg(test)]
588mod test {
589 use super::*;
590
591 fn tmp_dir() -> std::path::PathBuf {
592 let dir = std::env::temp_dir()
593 .join(format!("bark-wallet-{}", std::process::id()))
594 .join(format!("{}", std::time::SystemTime::now()
595 .duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
596 fs::create_dir_all(&dir).unwrap();
597 dir
598 }
599
600 fn failing_create_opts() -> CreateOpts {
603 CreateOpts {
604 force: false,
605 use_filestore: false,
606 mainnet: false,
607 regtest: false,
608 signet: true,
609 mutinynet: false,
610 mnemonic: None,
611 birthday_height: None,
612 config: ConfigOpts::default(),
613 }
614 }
615
616 #[test]
619 fn gap_limit_is_written_to_config() {
620 let dir = tmp_dir();
621 let path = dir.join(CONFIG_FILE);
622 let opts = ConfigOpts {
623 ark: Some("http://127.0.0.1:3535".into()),
624 esplora: Some("http://127.0.0.1:3002".into()),
625 gap_limit: Some(10_000),
626 ..ConfigOpts::default()
627 };
628
629 let config = opts.write_to_file(Network::Signet, &path)
630 .expect("writing the config should succeed");
631
632 assert_eq!(config.vtxo_key_gap_limit, 10_000, "the written config should carry the limit");
633 let written = fs::read_to_string(&path).expect("config file should exist");
634 assert!(written.contains("vtxo_key_gap_limit = 10000"), "unexpected config: {written}");
635
636 let bare_path = dir.join("bare.toml");
638 let bare = ConfigOpts { gap_limit: None, ..opts }
639 .write_to_file(Network::Signet, &bare_path)
640 .expect("writing the config should succeed");
641 assert_eq!(bare.vtxo_key_gap_limit, bark::DEFAULT_VTXO_KEY_GAP_LIMIT,
642 "without the flag the default should stand");
643 }
644
645 #[tokio::test]
646 async fn failed_create_keeps_barkd_files() {
647 let dir = tmp_dir();
648 fs::write(dir.join(AUTH_TOKEN_FILE), "tok").unwrap();
649 fs::write(dir.join(BARKD_LOCK_FILE), "42").unwrap();
650
651 create_wallet(&dir, "test", failing_create_opts()).await.unwrap_err();
652
653 assert!(dir.join(AUTH_TOKEN_FILE).exists());
654 assert!(dir.join(BARKD_LOCK_FILE).exists());
655
656 let _ = fs::remove_dir_all(&dir);
657 }
658
659 #[tokio::test]
660 async fn failed_create_removes_empty_datadir() {
661 let dir = tmp_dir();
662
663 create_wallet(&dir, "test", failing_create_opts()).await.unwrap_err();
664
665 assert!(!dir.exists());
666 }
667
668 #[test]
669 fn expected_datadir_files_and_their_temps() {
670 assert!(is_expected_datadir_file("auth_token"));
671 assert!(is_expected_datadir_file("barkd.lock"));
672 assert!(is_expected_datadir_file(".auth_token.temp.42.3.tmp"));
674 assert!(is_expected_datadir_file(".config.toml.temp.42.0.tmp"));
675
676 assert!(!is_expected_datadir_file("mnemonic"));
677 assert!(!is_expected_datadir_file("db.sqlite"));
678 assert!(!is_expected_datadir_file(".mnemonic.temp.42.0.tmp"));
679 assert!(!is_expected_datadir_file("barkd.lock.bak"));
680 }
681}
682