Skip to main content

bark_cli/
wallet.rs

1//! Wallet utilities
2//!
3//! Opens a Bark wallet and its on-chain companion from a data directory.
4//!
5//! ## Behavior
6//! - Reads a BIP-39 `mnemonic` file from the provided directory
7//! - Parses `config.toml` into a [`bark::Config`]
8//! - Opens `db.sqlite` as a [`bark::persist::sqlite::SqliteClient`] and loads persisted properties
9//! - Loads or creates the [`bark::onchain::OnchainWallet`], wraps it in
10//!   `Arc<RwLock<_>>`, and stores it inside the [`bark::Wallet`] via
11//!   [`bark::OpenWalletArgs::onchain`] — BDK-specific methods are accessed
12//!   via downcasting through [`bark::Wallet::onchain`]
13//! ## Errors
14//! Returns an [`anyhow::Error`] with context describing the failing step (I/O, parsing,
15//! database access, or wallet initialization).
16//!
17//! ## Example
18//! Open a wallet from a data directory:
19//!
20//! ```rust,no_run
21//! # use std::path::Path;
22//! # use bark_cli::wallet::open_wallet;
23//! # async fn example() -> anyhow::Result<()> {
24//!     let datadir = Path::new("./bark_data");
25//!     let bark_wallet = open_wallet(datadir, "myapp/1.0").await?.unwrap();
26//!     // Use the wallet...
27//!     Ok(())
28//! # }
29//! ```
30
31use 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
58/// File name of the mnemonic file.
59const MNEMONIC_FILE: &str = "mnemonic";
60
61/// File name of the database file.
62const DB_FILE: &str = DEFAULT_DB_FILE;
63/// File name of the filestore database file.
64const FILESTORE_FILE: &str = "wallet.json";
65
66/// File name of the config file.
67const CONFIG_FILE: &str = "config.toml";
68
69/// File name of the debug log file.
70const DEBUG_LOG_FILE: &str = "debug.log";
71
72/// File name used to persist the auth token in the datadir.
73pub const AUTH_TOKEN_FILE: &str = "auth_token";
74
75/// Process log files that may be written into the datadir by the daemon
76/// framework during testing; they should be ignored like debug.log.
77const STDOUT_LOG_FILE: &str = "stdout.log";
78const STDERR_LOG_FILE: &str = "stderr.log";
79
80/// Take the datadir lock via [`lock_manager::platform_default`], surfacing
81/// the "already held" case as-is so the CLI prints a clean error.
82fn 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/// Options to define the initial bark config
91#[derive(Clone, PartialEq, Eq, Default, clap::Args)]
92pub struct ConfigOpts {
93	/// The address of your Ark server.
94	#[arg(long)]
95	pub ark: Option<String>,
96
97	/// [DEPRECATED] The access token for a private server.
98	/// Access tokens are no longer enforced by the server; this flag will be removed.
99	#[arg(long, hide = true)]
100	pub access_token: Option<String>,
101
102	/// The address of the Esplora HTTP server to use.
103	///
104	/// Either this or the `bitcoind_address` field has to be provided.
105	#[arg(long)]
106	pub esplora: Option<String>,
107
108	/// The address of the bitcoind RPC server to use.
109	///
110	/// Either this or the `esplora_address` field has to be provided.
111	#[arg(long)]
112	pub bitcoind: Option<String>,
113
114	/// The path to the bitcoind rpc cookie file.
115	///
116	/// Only used with `bitcoind_address`.
117	#[arg(long)]
118	pub bitcoind_cookie: Option<String>,
119
120	/// The bitcoind RPC username.
121	///
122	/// Only used with `bitcoind_address`.
123	#[arg(long)]
124	pub bitcoind_user: Option<String>,
125
126	/// The bitcoind RPC password.
127	///
128	/// Only used with `bitcoind_address`.
129	#[arg(long)]
130	pub bitcoind_pass: Option<String>,
131
132	/// SOCKS5 proxy URL (e.g. socks5h://127.0.0.1:9050 for Tor).
133	/// Automatically bypassed for localhost connections.
134	#[arg(long)]
135	pub socks5_proxy: Option<String>,
136
137	/// How many consecutive unused key indices a VTXO key scan may cross before
138	/// it concludes the wallet doesn't own a recovered/imported VTXO.
139	#[arg(long)]
140	pub gap_limit: Option<u32>,
141}
142
143impl ConfigOpts {
144	/// Fill the default required config fields based on network
145	pub fn fill_network_defaults(&mut self, net: BarkNetwork) {
146		// Fallback to our default mainnet
147		if net == BarkNetwork::Mainnet {
148			// Only do it when the user did *not* specify either --esplora or --bitcoind.
149			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		// Fallback to our default signet
159		if net == BarkNetwork::Signet {
160			// Only do it when the user did *not* specify either --esplora or --bitcoind.
161			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		// Fallback to Mutinynet community Esplora
171		// Only do it when the user did *not* specify either --esplora or --bitcoind.
172		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	/// Validate the config options are sane
178	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	/// Will write the provided config options to the config
216	///
217	/// Will also load and return the config when loaded from the written file.
218	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		// new let's try load it to make sure it's sane
258		Ok(Config::load(network, path).context("problematic config flags provided")?)
259	}
260}
261
262#[derive(Args)]
263pub struct CreateOpts {
264	/// Force re-create the wallet even if it already exists.
265	/// Any funds in the old wallet will be lost
266	#[arg(long)]
267	pub force: bool,
268
269	/// Use filestore (JSON file) persistence instead of SQLite.
270	/// This creates a marker file so subsequent commands use the same backend.
271	///
272	/// Warning: do not use this for a production wallet.
273	#[arg(long)]
274	pub use_filestore: bool,
275
276	/// Use bitcoin mainnet
277	#[arg(long)]
278	pub mainnet: bool,
279	/// Use regtest network
280	#[arg(long)]
281	pub regtest: bool,
282	/// Use the official signet network
283	#[arg(long)]
284	pub signet: bool,
285	/// Use mutinynet
286	#[arg(long)]
287	pub mutinynet: bool,
288
289	/// Recover a wallet with an existing mnemonic.
290	/// This currently only works for on-chain funds.
291	#[arg(long)]
292	pub mnemonic: Option<bip39::Mnemonic>,
293
294	/// The wallet/mnemonic's birthday blockheight to start syncing when recovering.
295	#[arg(long)]
296	pub birthday_height: Option<BlockHeight>,
297
298	#[command(flatten)]
299	pub config: ConfigOpts,
300}
301
302/// Non-wallet files barkd leaves in the datadir; they must survive a wallet create.
303const 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
312/// Whether `name` is an expected datadir file or an atomic-write temp
313/// sibling of one (`.<name>.temp.<nanos>.<n>.tmp`).
314fn 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
323/// Checks the config file and maybe cleans it
324/// - returns whether a config file was present
325/// - if clean is false, errors if any file not config or logs is present
326/// - if clean is true, removes all files not config or logs
327async 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			// otherwise try wipe
346			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				// can't happen
353				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	// check for non-config file contents in the datadir and wipe if force
376	let config_existed = check_clean_datadir(datadir, opts.force).await?;
377
378	// Everything that errors after this will wipe the datadir again.
379	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			// A barkd serving this datadir must keep its lock and auth
387			// token; drop the datadir itself only when nothing is left.
388			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
399/// In this method we create the wallet and if it fails, the datadir will be wiped again.
400async 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		// Without config args the network defaults alone can suffice;
421		// validate rejects networks that lack a default chain source
422		// (e.g. regtest).
423		(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	// Default the wire-level client identity to the calling binary unless the
432	// operator pinned one in config.toml.
433	if config.user_agent.is_none() {
434		config.user_agent = Some(user_agent.to_owned());
435	}
436
437	// A mnemonic implies that the user wishes to recover an existing wallet.
438	if opts.mnemonic.is_some() {
439		if opts.birthday_height.is_none() {
440			// Only Bitcoin Core requires a birthday height to avoid syncing the entire chain.
441			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	// chain source client
454	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	// generate seed
461	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	// open db
470	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	// Skip initial block sync if we generated a new wallet.
485	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	// Create the wallet by opening with `create_if_not_exists` rather than
493	// calling `Wallet::create` directly: opening is what runs seed recovery, so
494	// a wallet restored from a mnemonic rebuilds its spendable VTXO set from the
495	// recovery mailbox. The on-chain scan above has already run, so recovery can
496	// validate recovered VTXOs against their chain anchors. `run_daemon` stays
497	// off — this only needs to create and recover.
498	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
514/// Read the raw mnemonic phrase from the wallet datadir.
515pub 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	// read mnemonic file
526	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	// The backend db (sqlite or filestore) is checked by its adaptor on open.
537	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	// Read the config
558	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	// Default the wire-level client identity to the calling binary unless the
563	// operator pinned one in config.toml.
564	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	/// Options that fail wallet creation with "provide config flags or a
601	/// config file", after the datadir checks.
602	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	/// --gap-limit is config, not a one-shot: it must land in config.toml so
617	/// later commands (imports, a re-created wallet) see the same limit.
618	#[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		// Omitting the flag leaves the network default in place.
637		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		// Atomic-write temp siblings, mid-write or left by a crash.
673		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