use std::path::Path;
use std::sync::Arc;
use std::str::FromStr;
use anyhow::{Context, bail};
use bark::persist::adaptor::StorageAdaptorWrapper;
use bitcoin::Network;
use clap::Args;
use log::{debug, info, warn};
use tonic::transport::Uri;
use bark::{BarkNetwork, Config, Wallet as BarkWallet};
use bark::onchain::OnchainWallet;
use bark::persist::BarkPersister;
use bark::pid_lock::LOCK_FILE;
use bark::persist::sqlite::SqliteClient;
use bark::persist::adaptor::filestore::FileStorageAdaptor;
use bitcoin_ext::BlockHeight;
use crate::util;
const MNEMONIC_FILE: &str = "mnemonic";
const DB_FILE: &str = "db.sqlite";
const FILESTORE_FILE: &str = "wallet.json";
const CONFIG_FILE: &str = "config.toml";
const DEBUG_LOG_FILE: &str = "debug.log";
pub const AUTH_TOKEN_FILE: &str = "auth_token";
const STDOUT_LOG_FILE: &str = "stdout.log";
const STDERR_LOG_FILE: &str = "stderr.log";
#[derive(Clone, PartialEq, Eq, Default, clap::Args)]
pub struct ConfigOpts {
#[arg(long)]
pub ark: Option<String>,
#[arg(long)]
pub esplora: Option<String>,
#[arg(long)]
pub bitcoind: Option<String>,
#[arg(long)]
pub bitcoind_cookie: Option<String>,
#[arg(long)]
pub bitcoind_user: Option<String>,
#[arg(long)]
pub bitcoind_pass: Option<String>,
#[arg(long)]
pub socks5_proxy: Option<String>,
}
impl ConfigOpts {
fn fill_network_defaults(&mut self, net: BarkNetwork) {
if net == BarkNetwork::Mainnet {
if self.esplora.is_none() && self.bitcoind.is_none() {
self.esplora = Some("https://mempool.second.tech/api".to_owned());
}
}
if net == BarkNetwork::Signet {
if self.esplora.is_none() && self.bitcoind.is_none() {
self.esplora = Some("https://esplora.signet.2nd.dev/".to_owned());
}
if self.ark.is_none() {
self.ark = Some("https://ark.signet.2nd.dev/".to_owned());
}
}
if net == BarkNetwork::Mutinynet && self.esplora.is_none() && self.bitcoind.is_none() {
self.esplora = Some("https://mutinynet.com/api".to_owned());
}
}
fn validate(&self) -> anyhow::Result<()> {
if self.esplora.is_none() && self.bitcoind.is_none() {
bail!("You need to provide a chain source using either --esplora or --bitcoind");
}
match (
self.bitcoind.is_some(),
self.bitcoind_cookie.is_some(),
self.bitcoind_user.is_some(),
self.bitcoind_pass.is_some(),
) {
(false, false, false, false) => {},
(false, _, _, _) => bail!("Provided bitcoind auth args without bitcoind address"),
(_, true, false, false) => {},
(_, true, _, _) => bail!("Bitcoind user/pass shouldn't be provided together with cookie file"),
(_, _, true, true) => {},
_ => bail!("When providing --bitcoind, you need to provide auth args as well."),
}
if let Some(ref proxy) = self.socks5_proxy {
let uri = proxy.parse::<Uri>().context("invalid socks5 proxy URI")?;
let scheme = uri.scheme_str().context("invalid socks5 proxy URI scheme")?;
if scheme != "socks5h" {
bail!("Only socks5h:// proxies are supported");
}
}
Ok(())
}
fn write_to_file(&self, network: Network, path: impl AsRef<Path>) -> anyhow::Result<Config> {
use std::fmt::Write;
let mut conf = String::new();
let ark = util::default_scheme("https", self.ark.as_ref().context("missing --ark arg")?)
.context("invalid ark server URL")?;
writeln!(conf, "server_address = \"{}\"", ark).unwrap();
if let Some(ref v) = self.esplora {
let url = util::default_scheme("https", v).context("invalid esplora URL")?;
writeln!(conf, "esplora_address = \"{}\"", url).unwrap();
}
if let Some(ref v) = self.bitcoind {
let url = util::default_scheme("http", v).context("invalid bitcoind URL")?;
writeln!(conf, "bitcoind_address = \"{}\"", url).unwrap();
}
if let Some(ref v) = self.bitcoind_cookie {
writeln!(conf, "bitcoind_cookiefile = \"{}\"", v).unwrap();
}
if let Some(ref v) = self.bitcoind_user {
writeln!(conf, "bitcoind_user = \"{}\"", v).unwrap();
}
if let Some(ref v) = self.bitcoind_pass {
writeln!(conf, "bitcoind_pass = \"{}\"", v).unwrap();
}
if let Some(ref v) = self.socks5_proxy {
writeln!(conf, "socks5_proxy = \"{}\"", v).unwrap();
}
let path = path.as_ref();
std::fs::write(path, conf).with_context(|| format!(
"error writing new config file to {}", path.display(),
))?;
Ok(Config::load(network, path).context("problematic config flags provided")?)
}
}
#[derive(Args)]
pub struct CreateOpts {
#[arg(long)]
pub force: bool,
#[arg(long)]
pub use_filestore: bool,
#[arg(long)]
pub mainnet: bool,
#[arg(long)]
pub regtest: bool,
#[arg(long)]
pub signet: bool,
#[arg(long)]
pub mutinynet: bool,
#[arg(long)]
pub mnemonic: Option<bip39::Mnemonic>,
#[arg(long)]
pub birthday_height: Option<BlockHeight>,
#[command(flatten)]
pub config: ConfigOpts,
}
async fn check_clean_datadir(datadir: &Path, clean: bool) -> anyhow::Result<bool> {
let mut has_config = false;
if datadir.exists() {
for item in datadir.read_dir().context("error accessing datadir")? {
let item = item.context("error reading existing content of datadir")?;
if item.file_name() == CONFIG_FILE {
has_config = true;
continue;
}
if item.file_name() == DEBUG_LOG_FILE
|| item.file_name() == STDOUT_LOG_FILE
|| item.file_name() == STDERR_LOG_FILE
{
continue;
}
if item.file_name() == LOCK_FILE {
continue;
}
if item.file_name() == AUTH_TOKEN_FILE {
continue;
}
if !clean {
bail!("Datadir has unexpected contents: {}", item.path().display());
}
let file_type = item.file_type().context("error accessing datadir content")?;
if file_type.is_dir() {
tokio::fs::remove_dir_all(item.path()).await.context("error deleting datadir content")?;
} else if file_type.is_file() || file_type.is_symlink() {
tokio::fs::remove_file(item.path()).await.context("error deleting datadir content")?;
} else {
bail!("non-existent file type in ");
}
}
}
Ok(has_config)
}
pub async fn create_wallet(datadir: &Path, opts: CreateOpts) -> anyhow::Result<()> {
debug!("Creating wallet in {}", datadir.display());
let net = match (opts.mainnet, opts.signet, opts.regtest, opts.mutinynet) {
(true, false, false, false) => BarkNetwork::Mainnet,
(false, true, false, false) => BarkNetwork::Signet,
(false, false, true, false) => BarkNetwork::Regtest,
(false, false, false, true ) => BarkNetwork::Mutinynet,
_ => bail!("Specify exactly one of --mainnet, --signet, --regtest or --mutinynet"),
};
let config_existed = check_clean_datadir(datadir, opts.force).await?;
let result = try_create_wallet(datadir, net, opts).await;
if let Err(e) = result {
if config_existed {
if let Err(e) = check_clean_datadir(datadir, true).await {
warn!("Error cleaning datadir after failure: {:#}", e);
}
} else {
if let Err(e) = tokio::fs::remove_dir_all(datadir).await {
warn!("Error removing datadir after failure: {:#}", e);
}
}
bail!("Error while creating wallet: {:#}", e);
}
Ok(())
}
async fn try_create_wallet(
datadir: &Path,
net: BarkNetwork,
mut opts: CreateOpts,
) -> anyhow::Result<()> {
info!("Creating new bark Wallet at {}", datadir.display());
tokio::fs::create_dir_all(datadir).await.context("can't create dir")?;
let config_path = datadir.join(CONFIG_FILE);
let has_config_args = opts.config != ConfigOpts::default();
let config = match (config_path.exists(), has_config_args) {
(true, false) => {
Config::load(net.as_bitcoin(), &config_path).with_context(|| format!(
"error loading existing config file at {}", config_path.display(),
))?
},
(false, true) => {
opts.config.fill_network_defaults(net);
opts.config.validate().context("invalid config options")?;
opts.config.write_to_file(net.as_bitcoin(), config_path)?
},
(false, false) => bail!("You need to provide config flags or a config file"),
(true, true) => bail!("Cannot provide an existing config file and config flags"),
};
if opts.mnemonic.is_some() {
if opts.birthday_height.is_none() {
if config.bitcoind_address.is_some() {
bail!("You need to set the --birthday-height field when recovering from mnemonic.");
}
} else if config.esplora_address.is_some() {
warn!("The given --birthday-height will be ignored because you're using Esplora.");
}
warn!("Recovering from mnemonic currently only supports recovering on-chain funds!");
} else {
if opts.birthday_height.is_some() {
bail!("Can't set --birthday-height if --mnemonic is not set.");
}
}
let is_new_wallet = opts.mnemonic.is_none();
let mnemonic = opts.mnemonic.unwrap_or_else(|| bip39::Mnemonic::generate(12).expect("12 is valid"));
let seed = mnemonic.to_seed("");
tokio::fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await
.context("failed to write mnemonic")?;
let db: Arc<dyn BarkPersister + Send + Sync> = if opts.use_filestore {
debug!("Using filestore backend");
let adaptor = FileStorageAdaptor::open(datadir.join(FILESTORE_FILE)).await?;
Arc::new(StorageAdaptorWrapper::new(adaptor))
} else {
debug!("Using sqlite backend");
Arc::new(SqliteClient::open(datadir.join(DB_FILE))?)
};
let mut onchain = OnchainWallet::load_or_create(net.as_bitcoin(), seed, db.clone()).await?;
let wallet = BarkWallet::create_with_onchain(
&mnemonic, net.as_bitcoin(), config, db, &onchain, opts.force,
).await.context("error creating wallet")?;
let birthday_height = if is_new_wallet {
Some(wallet.chain.tip().await?)
} else {
opts.birthday_height
};
onchain.initial_wallet_scan(&wallet.chain, birthday_height).await?;
Ok(())
}
pub async fn open_wallet(datadir: &Path) -> anyhow::Result<Option<(BarkWallet, OnchainWallet)>> {
debug!("Opening bark wallet in {}", datadir.display());
let mnemonic_path = datadir.join(MNEMONIC_FILE);
if !tokio::fs::try_exists(datadir).await? {
return Ok(None);
}
if !tokio::fs::try_exists(&mnemonic_path).await? {
return Ok(None);
}
let mnemonic_str = tokio::fs::read_to_string(&mnemonic_path).await
.with_context(|| format!("failed to read mnemonic file at {}", mnemonic_path.display()))?;
let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).context("broken mnemonic")?;
let seed = mnemonic.to_seed("");
let use_filestore = datadir.join(FILESTORE_FILE).exists();
let db: Arc<dyn BarkPersister + Send + Sync> = if use_filestore {
debug!("Using filestore backend");
let adaptor = FileStorageAdaptor::open(datadir.join(FILESTORE_FILE)).await?;
Arc::new(StorageAdaptorWrapper::new(adaptor))
} else {
debug!("Using sqlite backend");
Arc::new(SqliteClient::open(datadir.join(DB_FILE))?)
};
let properties = db.read_properties().await?.context("failed to read properties")?;
let config_path = datadir.join("config.toml");
let config = Config::load(properties.network, config_path)
.context("error loading bark config file")?;
let bdk_wallet = OnchainWallet::load_or_create(properties.network, seed, db.clone()).await?;
let bark_wallet = BarkWallet::open_with_onchain(&mnemonic, db, &bdk_wallet, config).await?;
if let Err(e) = bark_wallet.require_chainsource_version() {
warn!("{}", e);
}
Ok(Some((bark_wallet, bdk_wallet)))
}