1use std::path::Path;
32use std::sync::Arc;
33use std::str::FromStr;
34
35use anyhow::{Context, bail};
36use bark::persist::adaptor::StorageAdaptorWrapper;
37use bitcoin::Network;
38use clap::Args;
39use log::{debug, info, warn};
40use tonic::transport::Uri;
41
42use bark::{BarkNetwork, Config, Wallet as BarkWallet};
43use bark::lock_manager::{self, LockManager, PidLockError};
44use bark::lock_manager::pid_flock::LOCK_FILE;
45use bark::onchain::OnchainWallet;
46use bark::persist::BarkPersister;
47use bark::persist::sqlite::SqliteClient;
48use bark::persist::adaptor::filestore::FileStorageAdaptor;
49
50use bitcoin_ext::BlockHeight;
51
52use crate::util;
53
54const MNEMONIC_FILE: &str = "mnemonic";
56
57const DB_FILE: &str = "db.sqlite";
59const FILESTORE_FILE: &str = "wallet.json";
61
62const CONFIG_FILE: &str = "config.toml";
64
65const DEBUG_LOG_FILE: &str = "debug.log";
67
68pub const AUTH_TOKEN_FILE: &str = "auth_token";
70
71const STDOUT_LOG_FILE: &str = "stdout.log";
74const STDERR_LOG_FILE: &str = "stderr.log";
75
76fn open_lock_manager(datadir: &Path) -> anyhow::Result<Box<dyn LockManager>> {
79 match lock_manager::platform_default(datadir) {
80 Ok(m) => Ok(m),
81 Err(e) if e.is::<PidLockError>() => Err(e),
82 Err(e) => Err(e.context("failed to acquire datadir lock")),
83 }
84}
85
86#[derive(Clone, PartialEq, Eq, Default, clap::Args)]
88pub struct ConfigOpts {
89 #[arg(long)]
91 pub ark: Option<String>,
92
93 #[arg(long)]
95 pub access_token: Option<String>,
96
97 #[arg(long)]
101 pub esplora: Option<String>,
102
103 #[arg(long)]
107 pub bitcoind: Option<String>,
108
109 #[arg(long)]
113 pub bitcoind_cookie: Option<String>,
114
115 #[arg(long)]
119 pub bitcoind_user: Option<String>,
120
121 #[arg(long)]
125 pub bitcoind_pass: Option<String>,
126
127 #[arg(long)]
130 pub socks5_proxy: Option<String>,
131}
132
133impl ConfigOpts {
134 fn fill_network_defaults(&mut self, net: BarkNetwork) {
136 if net == BarkNetwork::Mainnet {
138 if self.esplora.is_none() && self.bitcoind.is_none() {
140 self.esplora = Some("https://mempool.second.tech/api".to_owned());
141 }
142 }
143
144 if net == BarkNetwork::Signet {
146 if self.esplora.is_none() && self.bitcoind.is_none() {
148 self.esplora = Some("https://esplora.signet.2nd.dev/".to_owned());
149 }
150
151 if self.ark.is_none() {
152 self.ark = Some("https://ark.signet.2nd.dev/".to_owned());
153 }
154 }
155
156 if net == BarkNetwork::Mutinynet && self.esplora.is_none() && self.bitcoind.is_none() {
159 self.esplora = Some("https://mutinynet.com/api".to_owned());
160 }
161 }
162
163 fn validate(&self) -> anyhow::Result<()> {
165 if self.esplora.is_none() && self.bitcoind.is_none() {
166 bail!("You need to provide a chain source using either --esplora or --bitcoind");
167 }
168
169 match (
170 self.bitcoind.is_some(),
171 self.bitcoind_cookie.is_some(),
172 self.bitcoind_user.is_some(),
173 self.bitcoind_pass.is_some(),
174 ) {
175 (false, false, false, false) => {},
176 (false, _, _, _) => bail!("Provided bitcoind auth args without bitcoind address"),
177 (_, true, false, false) => {},
178 (_, true, _, _) => bail!("Bitcoind user/pass shouldn't be provided together with cookie file"),
179 (_, _, true, true) => {},
180 _ => bail!("When providing --bitcoind, you need to provide auth args as well."),
181 }
182
183 if let Some(ref proxy) = self.socks5_proxy {
184 let uri = proxy.parse::<Uri>().context("invalid socks5 proxy URI")?;
185 let scheme = uri.scheme_str().context("invalid socks5 proxy URI scheme")?;
186 if scheme != "socks5h" {
187 bail!("Only socks5h:// proxies are supported");
188 }
189 }
190
191 Ok(())
192 }
193
194 fn write_to_file(&self, network: Network, path: impl AsRef<Path>) -> anyhow::Result<Config> {
198 use std::fmt::Write;
199
200 let mut conf = String::new();
201 let ark = util::default_scheme("https", self.ark.as_ref().context("missing --ark arg")?)
202 .context("invalid ark server URL")?;
203 writeln!(conf, "server_address = \"{}\"", ark).unwrap();
204
205 if let Some(ref v) = self.access_token {
206 writeln!(conf, "server_access_token = \"{}\"", v).unwrap();
207 }
208 if let Some(ref v) = self.esplora {
209 let url = util::default_scheme("https", v).context("invalid esplora URL")?;
210 writeln!(conf, "esplora_address = \"{}\"", url).unwrap();
211 }
212 if let Some(ref v) = self.bitcoind {
213 let url = util::default_scheme("http", v).context("invalid bitcoind URL")?;
214 writeln!(conf, "bitcoind_address = \"{}\"", url).unwrap();
215 }
216 if let Some(ref v) = self.bitcoind_cookie {
217 writeln!(conf, "bitcoind_cookiefile = \"{}\"", v).unwrap();
218 }
219 if let Some(ref v) = self.bitcoind_user {
220 writeln!(conf, "bitcoind_user = \"{}\"", v).unwrap();
221 }
222 if let Some(ref v) = self.bitcoind_pass {
223 writeln!(conf, "bitcoind_pass = \"{}\"", v).unwrap();
224 }
225 if let Some(ref v) = self.socks5_proxy {
226 writeln!(conf, "socks5_proxy = \"{}\"", v).unwrap();
227 }
228
229 let path = path.as_ref();
230 std::fs::write(path, conf).with_context(|| format!(
231 "error writing new config file to {}", path.display(),
232 ))?;
233
234 Ok(Config::load(network, path).context("problematic config flags provided")?)
236 }
237}
238
239#[derive(Args)]
240pub struct CreateOpts {
241 #[arg(long)]
244 pub force: bool,
245
246 #[arg(long)]
251 pub use_filestore: bool,
252
253 #[arg(long)]
255 pub mainnet: bool,
256 #[arg(long)]
258 pub regtest: bool,
259 #[arg(long)]
261 pub signet: bool,
262 #[arg(long)]
264 pub mutinynet: bool,
265
266 #[arg(long)]
269 pub mnemonic: Option<bip39::Mnemonic>,
270
271 #[arg(long)]
273 pub birthday_height: Option<BlockHeight>,
274
275 #[command(flatten)]
276 pub config: ConfigOpts,
277}
278
279async fn check_clean_datadir(datadir: &Path, clean: bool) -> anyhow::Result<bool> {
284 let mut has_config = false;
285 if datadir.exists() {
286 for item in datadir.read_dir().context("error accessing datadir")? {
287 let item = item.context("error reading existing content of datadir")?;
288
289 if item.file_name() == CONFIG_FILE {
290 has_config = true;
291 continue;
292 }
293 if item.file_name() == DEBUG_LOG_FILE
294 || item.file_name() == STDOUT_LOG_FILE
295 || item.file_name() == STDERR_LOG_FILE
296 {
297 continue;
298 }
299 if item.file_name() == LOCK_FILE {
300 continue;
301 }
302 if item.file_name() == AUTH_TOKEN_FILE {
303 continue;
304 }
305
306 if !clean {
307 bail!("Datadir has unexpected contents: {}", item.path().display());
308 }
309
310 let file_type = item.file_type().context("error accessing datadir content")?;
312 if file_type.is_dir() {
313 tokio::fs::remove_dir_all(item.path()).await.context("error deleting datadir content")?;
314 } else if file_type.is_file() || file_type.is_symlink() {
315 tokio::fs::remove_file(item.path()).await.context("error deleting datadir content")?;
316 } else {
317 bail!("non-existent file type in ");
319 }
320 }
321 }
322 Ok(has_config)
323}
324
325pub async fn create_wallet(datadir: &Path, opts: CreateOpts) -> anyhow::Result<()> {
326 debug!("Creating wallet in {}", datadir.display());
327
328 let net = match (opts.mainnet, opts.signet, opts.regtest, opts.mutinynet) {
329 (true, false, false, false) => BarkNetwork::Mainnet,
330 (false, true, false, false) => BarkNetwork::Signet,
331 (false, false, true, false) => BarkNetwork::Regtest,
332 (false, false, false, true ) => BarkNetwork::Mutinynet,
333 _ => bail!("Specify exactly one of --mainnet, --signet, --regtest or --mutinynet"),
334 };
335
336 let config_existed = check_clean_datadir(datadir, opts.force).await?;
338
339 let result = try_create_wallet(datadir, net, opts).await;
341 if let Err(e) = result {
342 if config_existed {
343 if let Err(e) = check_clean_datadir(datadir, true).await {
344 warn!("Error cleaning datadir after failure: {:#}", e);
345 }
346 } else {
347 if let Err(e) = tokio::fs::remove_dir_all(datadir).await {
348 warn!("Error removing datadir after failure: {:#}", e);
349 }
350 }
351
352 bail!("Error while creating wallet: {:#}", e);
353 }
354 Ok(())
355}
356
357async fn try_create_wallet(
359 datadir: &Path,
360 net: BarkNetwork,
361 mut opts: CreateOpts,
362) -> anyhow::Result<()> {
363 info!("Creating new bark Wallet at {}", datadir.display());
364
365 tokio::fs::create_dir_all(datadir).await.context("can't create dir")?;
366
367 let config_path = datadir.join(CONFIG_FILE);
368 let has_config_args = opts.config != ConfigOpts::default();
369 let config = match (config_path.exists(), has_config_args) {
370 (true, false) => {
371 Config::load(net.as_bitcoin(), &config_path).with_context(|| format!(
372 "error loading existing config file at {}", config_path.display(),
373 ))?
374 },
375 (false, true) => {
376 opts.config.fill_network_defaults(net);
377 opts.config.validate().context("invalid config options")?;
378 opts.config.write_to_file(net.as_bitcoin(), config_path)?
379 },
380 (false, false) => bail!("You need to provide config flags or a config file"),
381 (true, true) => bail!("Cannot provide an existing config file and config flags"),
382 };
383
384 if opts.mnemonic.is_some() {
386 if opts.birthday_height.is_none() {
387 if config.bitcoind_address.is_some() {
389 bail!("You need to set the --birthday-height field when recovering from mnemonic.");
390 }
391 } else if config.esplora_address.is_some() {
392 warn!("The given --birthday-height will be ignored because you're using Esplora.");
393 }
394 warn!("Recovering from mnemonic currently only supports recovering on-chain funds!");
395 } else {
396 if opts.birthday_height.is_some() {
397 bail!("Can't set --birthday-height if --mnemonic is not set.");
398 }
399 }
400
401 let is_new_wallet = opts.mnemonic.is_none();
403 let mnemonic = opts.mnemonic.unwrap_or_else(|| bip39::Mnemonic::generate(12).expect("12 is valid"));
404 let seed = mnemonic.to_seed("");
405 tokio::fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await
406 .context("failed to write mnemonic")?;
407
408 let db: Arc<dyn BarkPersister + Send + Sync> = if opts.use_filestore {
410 debug!("Using filestore backend");
411 let adaptor = FileStorageAdaptor::open(datadir.join(FILESTORE_FILE)).await?;
412 Arc::new(StorageAdaptorWrapper::new(adaptor))
413 } else {
414 debug!("Using sqlite backend");
415 Arc::new(SqliteClient::open(datadir.join(DB_FILE))?)
416 };
417
418 let mut onchain = OnchainWallet::load_or_create(net.as_bitcoin(), seed, db.clone()).await?;
419 let lock_manager = open_lock_manager(&datadir)?;
420 let wallet = BarkWallet::create_with_exits(
421 &mnemonic, net.as_bitcoin(), config, db, lock_manager, opts.force,
422 ).await.context("error creating wallet")?;
423
424 let birthday_height = if is_new_wallet {
426 Some(wallet.chain().tip().await?)
427 } else {
428 opts.birthday_height
429 };
430 onchain.initial_wallet_scan(wallet.chain(), birthday_height).await?;
431 Ok(())
432}
433
434pub async fn open_wallet(datadir: &Path) -> anyhow::Result<Option<(BarkWallet, OnchainWallet)>> {
435 debug!("Opening bark wallet in {}", datadir.display());
436
437
438 let mnemonic_path = datadir.join(MNEMONIC_FILE);
440
441 if !tokio::fs::try_exists(datadir).await? {
442 return Ok(None);
443 }
444
445 if !tokio::fs::try_exists(&mnemonic_path).await? {
446 return Ok(None);
447 }
448
449 let mnemonic_str = tokio::fs::read_to_string(&mnemonic_path).await
450 .with_context(|| format!("failed to read mnemonic file at {}", mnemonic_path.display()))?;
451 let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).context("broken mnemonic")?;
452 let seed = mnemonic.to_seed("");
453
454 let use_filestore = datadir.join(FILESTORE_FILE).exists();
455 let db: Arc<dyn BarkPersister + Send + Sync> = if use_filestore {
456 debug!("Using filestore backend");
457 let adaptor = FileStorageAdaptor::open(datadir.join(FILESTORE_FILE)).await?;
458 Arc::new(StorageAdaptorWrapper::new(adaptor))
459 } else {
460 debug!("Using sqlite backend");
461 Arc::new(SqliteClient::open(datadir.join(DB_FILE))?)
462 };
463 let properties = db.read_properties().await?.context("failed to read properties")?;
464
465 let config_path = datadir.join("config.toml");
467 let config = Config::load(properties.network, config_path)
468 .context("error loading bark config file")?;
469
470 let bdk_wallet = OnchainWallet::load_or_create(properties.network, seed, db.clone()).await?;
471 let lock_manager = open_lock_manager(datadir)?;
472 let bark_wallet = BarkWallet::open_with_exits(&mnemonic, db, config, lock_manager).await?;
473
474 if let Err(e) = bark_wallet.require_chainsource_version().await {
475 warn!("{}", e);
476 }
477
478 Ok(Some((bark_wallet, bdk_wallet)))
479}
480