use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use anyhow::Context;
use clap;
use log::{debug, info, warn};
use ark::{ArkInfo, Vtxo, VtxoId};
use ark::encode::ProtocolEncoding;
use ark::vtxo::Full;
use bark::ImportVtxoArgs;
use bark::vtxo::VtxoStateKind;
use bark_json::primitives::{VtxoInfo, WalletVtxoInfo};
use server_rpc as rpc;
use bark_cli::wallet::open_wallet;
use bark_cli::util::{self, output_json};
#[derive(clap::Subcommand)]
pub enum DevCommand {
#[command(subcommand)]
Vtxo(VtxoCommand),
#[command()]
ArkInfo {
ark_address: String,
},
}
pub async fn execute_dev_command(
command: DevCommand,
datadir: PathBuf,
) -> anyhow::Result<()> {
match command {
DevCommand::Vtxo(c) => execute_vtxo_command(&datadir, c).await?,
DevCommand::ArkInfo { ark_address } => {
let mut srv = connect_server(ark_address).await
.context("failed to connect to server")?;
let res = srv.get_ark_info(rpc::protos::Empty {}).await
.context("ark_info request failed")?;
let info = ArkInfo::try_from(res.into_inner())
.context("invalid ark info from ark server")?;
output_json(&bark_json::cli::ArkInfo::from(info));
},
}
Ok(())
}
#[derive(clap::Subcommand)]
pub enum VtxoCommand {
#[command()]
Decode {
vtxo: String,
},
#[command()]
Drop {
#[arg(long = "dangerous")]
dangerous: bool,
#[arg(long = "all")]
all: bool,
#[arg(long= "vtxo")]
vtxo: Vec<VtxoId>,
},
#[command()]
TrustAndAdoptServerStatus {
#[arg(long = "dangerous")]
dangerous: bool,
#[arg(long = "all", conflicts_with = "vtxos")]
all: bool,
vtxos: Vec<VtxoId>,
},
#[command()]
Import {
vtxos: Vec<String>,
#[arg(long = "vtxo", hide = true)]
vtxo_multi: Vec<String>,
#[arg(long = "gap-limit")]
gap_limit: Option<u32>,
#[arg(long = "skip-status-check")]
skip_status_check: bool,
#[arg(long = "allow-partial")]
allow_partial: bool,
},
}
async fn execute_vtxo_command(datadir: &Path, command: VtxoCommand) -> anyhow::Result<()> {
match command {
VtxoCommand::Decode { vtxo } => {
let vtxo = <Vtxo<Full>>::deserialize_hex(&vtxo).context("invalid vtxo")?;
debug!("{:#?}", vtxo);
let info = VtxoInfo::from(vtxo);
output_json(&info);
},
VtxoCommand::Drop { dangerous, all, vtxo} => {
if !dangerous {
bail!("You must acknowledge the danger. Run again with --dangerous")
}
let wallet = open_wallet(&datadir, crate::USER_AGENT).await
.context("Failed to open wallet")?
.context("No wallet found")?;
if all {
log::info!("Dropping all vtxos");
wallet.dangerous_drop_all_vtxos().await
.context("Failed to drop vtxos")?;
}
for v in vtxo {
log::info!("Dropping vtxo {}", v);
wallet.dangerous_drop_vtxo(v).await
.context("Failed to drop vtxo")?;
}
}
VtxoCommand::TrustAndAdoptServerStatus { dangerous, all, vtxos } => {
if !dangerous {
bail!("You must acknowledge the danger. Run again with --dangerous")
}
if !all && vtxos.is_empty() {
bail!("Pass either --all or a list of vtxo ids");
}
let wallet = open_wallet(&datadir, crate::USER_AGENT).await
.context("Failed to open wallet")?
.context("No wallet found")?;
let ids = if all {
wallet.all_vtxos().await.context("Failed to list vtxos")?
.iter()
.filter(|v| v.state.kind() != VtxoStateKind::Exited)
.map(|v| v.id())
.collect()
} else {
vtxos
};
let mut updated = Vec::with_capacity(ids.len());
let mut failed = 0;
for vtxo_id in ids {
match wallet.trust_and_adopt_server_vtxo_status(vtxo_id).await {
Ok(Some(adoption)) => info!("Server reports vtxo {} as {:?}", vtxo_id, adoption),
Ok(None) => info!("Vtxo {} is locked, leaving it alone", vtxo_id),
Err(e) => {
warn!("Failed to check vtxo {}: {:#}", vtxo_id, e);
failed += 1;
continue;
},
}
match wallet.get_vtxo_by_id(vtxo_id).await {
Ok(wallet_vtxo) => updated.push(WalletVtxoInfo::from(&wallet_vtxo)),
Err(e) => {
warn!("Failed to get vtxo {}: {:#}", vtxo_id, e);
failed += 1;
},
}
}
output_json(&updated);
if failed > 0 {
bail!("Failed to update {} vtxos", failed);
}
},
VtxoCommand::Import { vtxos, vtxo_multi, gap_limit, skip_status_check, allow_partial } => {
if vtxos.is_empty() && vtxo_multi.is_empty() {
bail!("No VTXOs provided. Add raw VTXO arguments to import");
}
if !vtxo_multi.is_empty() {
warn!("The --vtxo flag is deprecated. You can pass arguments \
directly without the flag.");
}
let mut to_import = Vec::with_capacity(vtxos.len() + vtxo_multi.len());
for vtxo_hex in vtxos.into_iter().chain(vtxo_multi) {
let vtxo = Vtxo::deserialize_hex(&vtxo_hex)
.with_context(|| format!("invalid vtxo: {}", vtxo_hex))?;
to_import.push(vtxo);
}
let wallet = open_wallet(&datadir, crate::USER_AGENT).await
.context("Failed to open wallet")?
.context("No wallet found")?;
info!("Importing {} VTXOs...", to_import.len());
let args = ImportVtxoArgs {
gap_limit, skip_status_check, allow_partial,
};
let ids = wallet.import_vtxos(&to_import, args).await
.context("Failed to import vtxos")?;
let mut imported = Vec::with_capacity(ids.len());
for vtxo_id in ids {
let wallet_vtxo = wallet.get_vtxo_by_id(vtxo_id).await
.with_context(|| format!("Failed to get imported vtxo {}", vtxo_id))?;
imported.push(WalletVtxoInfo::from(&wallet_vtxo));
}
output_json(&imported);
}
}
Ok(())
}
fn create_server_endpoint(address: &str) -> anyhow::Result<tonic::transport::Endpoint> {
let uri = tonic::transport::Uri::from_str(address)
.context("failed to parse Ark server as a URI")?;
let scheme = uri.scheme_str().unwrap_or("");
if scheme != "http" && scheme != "https" {
bail!("Ark server scheme must be either http or https. Found: {}", scheme);
}
let mut endpoint = tonic::transport::Channel::builder(uri.clone())
.keep_alive_timeout(Duration::from_secs(600))
.timeout(Duration::from_secs(600));
if scheme == "https" {
info!("Connecting to Ark server at {} using TLS...", address);
let uri_auth = uri.clone().into_parts().authority
.context("Ark server URI is missing an authority part")?;
let domain = uri_auth.host();
let tls_config = tonic::transport::ClientTlsConfig::new()
.with_enabled_roots()
.domain_name(domain);
endpoint = endpoint.tls_config(tls_config)?
} else {
info!("Connecting to Ark server at {} without TLS...", address);
};
Ok(endpoint)
}
pub async fn connect_server(
address: String,
) -> anyhow::Result<rpc::ArkServiceClient<tonic::transport::Channel>> {
let address = util::default_scheme("https", address)?;
let endpoint = create_server_endpoint(&address)?;
let channel = endpoint.connect().await
.context("couldn't connect to Ark server")?;
Ok(rpc::ArkServiceClient::new(channel))
}