use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use std::sync::Arc;
use std::str::FromStr;
use anyhow::Context;
use bark::persist::BarkPersister;
use bitcoin::Network;
use clap::Args;
use log::{debug, info, warn};
use tokio::fs;
use bark::{Config, Wallet as BarkWallet, SqliteClient};
use bark::onchain::OnchainWallet;
use bitcoin_ext::BlockHeight;
use crate::ConfigOpts;
const MNEMONIC_FILE: &str = "mnemonic";
const DB_FILE: &str = "db.sqlite";
const CONFIG_FILE: &str = "config.toml";
#[derive(Args)]
pub struct CreateOpts {
#[arg(long)]
force: bool,
#[arg(long)]
mainnet: bool,
#[arg(long)]
regtest: bool,
#[arg(long)]
signet: bool,
#[arg(long)]
mutinynet: bool,
#[arg(long)]
mnemonic: Option<bip39::Mnemonic>,
#[arg(long)]
birthday_height: Option<BlockHeight>,
#[command(flatten)]
config: ConfigOpts,
}
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) => Network::Bitcoin,
(false, true, false, false) => Network::Signet,
(false, false, true, false) => Network::Regtest,
(false, false, false, true ) => Network::Signet, _ => bail!("Specify exactly one of --mainnet, --signet, --regtest or --mutinynet"),
};
let mut config = Config {
server_address: opts.config.ark.clone().context("Ark server address missing, use --ark")?,
..Config::network_default(net)
};
if opts.signet && opts.config.esplora.is_none() && opts.config.bitcoind.is_none() {
config.esplora_address = Some("https://esplora.signet.2nd.dev/".to_owned());
}
if opts.mutinynet && opts.config.esplora.is_none() && opts.config.bitcoind.is_none() {
config.esplora_address = Some("https://mutinynet.com/api".to_owned());
}
opts.config.merge_into(&mut config).context("invalid configuration")?;
if datadir.exists() {
if opts.force {
fs::remove_dir_all(datadir).await?;
} else {
bail!("Directory {} already exists", datadir.display());
}
}
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 result = try_create_wallet(
&datadir, net, config, opts.mnemonic, opts.birthday_height, opts.force,
).await;
if let Err(e) = result {
if datadir.exists() {
if let Err(e) = fs::remove_dir_all(datadir).await {
warn!("Failed to remove '{}", datadir.display());
warn!("{}", e.to_string());
}
}
bail!("Error while creating wallet: {:?}", e);
}
Ok(())
}
async fn try_create_wallet(
datadir: &Path,
net: Network,
config: Config,
mnemonic: Option<bip39::Mnemonic>,
birthday_height: Option<BlockHeight>,
force: bool,
) -> anyhow::Result<()> {
info!("Creating new bark Wallet at {}", datadir.display());
fs::create_dir_all(datadir).await.context("can't create dir")?;
let is_new_wallet = mnemonic.is_none();
let mnemonic = mnemonic.unwrap_or_else(|| bip39::Mnemonic::generate(12).expect("12 is valid"));
let seed = mnemonic.to_seed("");
fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await
.context("failed to write mnemonic")?;
let toml_string = toml::to_string_pretty(&config).expect("config serialization error");
let config_path = datadir.join(CONFIG_FILE);
let mut file = File::create(&config_path)?;
write!(file, "{}", toml_string)
.with_context(|| format!("Failed to write config to {}", config_path.display()))?;
let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE))?);
let mut onchain = OnchainWallet::load_or_create(net, seed, db.clone())?;
let wallet = BarkWallet::create_with_onchain(&mnemonic, net, config, db, &onchain, force).await.context("error creating wallet")?;
let birthday_height = if is_new_wallet {
Some(wallet.chain.tip().await?)
} else {
birthday_height
};
onchain.initial_wallet_scan(&wallet.chain, birthday_height).await?;
Ok(())
}
pub async fn open_wallet(datadir: &Path) -> anyhow::Result<(BarkWallet, OnchainWallet)> {
debug!("Opening bark wallet in {}", datadir.display());
let mnemonic_path = datadir.join(MNEMONIC_FILE);
let mnemonic_str = 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 config_path = datadir.join("config.toml");
let mut config_file = File::open(&config_path)
.with_context(|| format!("Failed to open config file at {}", config_path.display()))?;
let mut config_str = String::new();
config_file.read_to_string(&mut config_str)
.with_context(|| format!("Failed to read config file at {}", config_path.display()))?;
let config: Config = toml::from_str(&config_str)
.with_context(|| format!("Failed to parse config file at {}", config_path.display()))?;
let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE))?);
let properties = db.read_properties()?.context("failed to read properties")?;
let bdk_wallet = OnchainWallet::load_or_create(properties.network, seed, db.clone())?;
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((bark_wallet, bdk_wallet))
}