#[macro_use] extern crate anyhow;
mod dev;
mod exit;
mod lightning;
mod onchain;
mod util;
mod wallet;
use std::cmp::Ordering;
use std::{env, process};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::Context;
use bitcoin::{Amount, Network};
use clap::builder::BoolishValueParser;
use clap::Parser;
use ::lightning::offers::offer::Offer;
use lightning_invoice::Bolt11Invoice;
use lnurl::lightning_address::LightningAddress;
use log::{debug, info, warn};
use ark::VtxoId;
use bark::{BarkNetwork, Config};
use bark::lightning_utils::{pay_invoice, pay_lnaddr, pay_offer};
use bark::round::RoundStatus;
use bark::vtxo::selection::VtxoFilter;
use bark::vtxo::state::VtxoStateKind;
use bark_json::{cli as json};
use bark_json::primitives::WalletVtxoInfo;
use bark_cli::wallet::open_wallet;
use bark_cli::log::init_logging;
use crate::util::output_json;
use crate::wallet::{CreateOpts, create_wallet};
const DEBUG_LOG_FILE: &str = "debug.log";
fn default_datadir() -> String {
home::home_dir().or_else(|| {
env::current_dir().ok()
}).unwrap_or_else(|| {
"./".into()
}).join(".bark").display().to_string()
}
const FULL_VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")");
fn round_status_to_json(status: &RoundStatus) -> json::RoundStatus {
match status {
RoundStatus::Confirmed { funding_txid } => {
json::RoundStatus::Confirmed { funding_txid: *funding_txid }
},
RoundStatus::Unconfirmed { funding_txid } => {
json::RoundStatus::Unconfirmed { funding_txid: *funding_txid }
},
RoundStatus::Pending { unsigned_funding_txids } => {
json::RoundStatus::Pending { unsigned_funding_txids: unsigned_funding_txids.clone() }
},
RoundStatus::Failed { error } => {
json::RoundStatus::Failed { error: error.clone() }
},
}
}
#[derive(Parser)]
#[command(name = "bark", author = "Team Second <hello@second.tech>", version = FULL_VERSION, about)]
struct Cli {
#[arg(
long,
short = 'v',
env = "BARK_VERBOSE",
global = true,
value_parser = BoolishValueParser::new(),
)]
verbose: bool,
#[arg(
long,
short = 'q',
env = "BARK_QUIET",
global = true,
value_parser = BoolishValueParser::new(),
)]
quiet: bool,
#[arg(long, env = "BARK_DATADIR", global = true, default_value_t = default_datadir())]
datadir: String,
#[command(subcommand)]
command: Command,
}
#[derive(Clone, PartialEq, Eq, Default, clap::Args)]
struct ConfigOpts {
#[arg(long)]
ark: Option<String>,
#[arg(long)]
esplora: Option<String>,
#[arg(long)]
bitcoind: Option<String>,
#[arg(long)]
bitcoind_cookie: Option<String>,
#[arg(long)]
bitcoind_user: Option<String>,
#[arg(long)]
bitcoind_pass: Option<String>,
}
impl ConfigOpts {
fn fill_network_defaults(&mut self, net: BarkNetwork) {
if net == BarkNetwork::Signet && self.esplora.is_none() && self.bitcoind.is_none() {
self.esplora = Some("https://esplora.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."),
}
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();
}
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(clap::Subcommand)]
enum Command {
#[command()]
Create(CreateOpts),
#[command()]
Config,
#[command()]
ArkInfo,
#[command()]
Address {
#[arg(long)]
index: Option<u32>,
},
#[command()]
Balance {
#[arg(long)]
no_sync: bool,
},
#[command()]
Vtxos {
#[arg(long)]
no_sync: bool,
#[arg(long)]
all: bool,
},
#[command()]
Movements {
#[arg(long)]
no_sync: bool,
},
#[command()]
Refresh {
#[arg(long = "vtxo", value_name = "VTXO_ID")]
vtxos: Option<Vec<String>>,
#[arg(long)]
threshold_blocks: Option<u32>,
#[arg(long)]
threshold_hours: Option<u32>,
#[arg(long)]
all: bool,
#[arg(long)]
counterparty: bool,
#[arg(long)]
no_sync: bool,
},
#[command()]
Board {
amount: Option<Amount>,
#[arg(long)]
all: bool,
#[arg(long)]
no_sync: bool,
},
#[command()]
Send {
destination: String,
amount: Option<Amount>,
comment: Option<String>,
#[arg(long)]
no_sync: bool,
},
#[command()]
SendOnchain {
destination: String,
amount: Amount,
#[arg(long)]
no_sync: bool,
},
#[command()]
Offboard {
#[arg(long)]
address: Option<String>,
#[arg(long = "vtxo", value_name = "VTXO_ID")]
vtxos: Option<Vec<String>>,
#[arg(long)]
all: bool,
#[arg(long)]
no_sync: bool,
},
#[command(subcommand)]
Onchain(onchain::OnchainCommand),
#[command(subcommand)]
Exit(exit::ExitCommand),
#[command(subcommand, visible_alias = "ln")]
Lightning(lightning::LightningCommand),
#[command(subcommand)]
Dev(dev::DevCommand),
#[command()]
Maintain,
}
async fn inner_main(cli: Cli) -> anyhow::Result<()> {
let datadir = PathBuf::from_str(&cli.datadir).unwrap();
debug!("Using bark datadir at {}", datadir.display());
init_logging(cli.verbose, cli.quiet, &datadir);
if let Command::Create(opts) = cli.command {
create_wallet(&datadir, opts).await?;
return Ok(())
}
if let Command::Dev(cmd) = cli.command {
return dev::execute_dev_command(cmd, datadir).await;
}
let (mut wallet, mut onchain) = open_wallet(&datadir).await.context("error opening wallet")?;
let net = wallet.properties()?.network;
match cli.command {
Command::Create { .. } | Command::Dev(_) => unreachable!("handled earlier"),
Command::Config => {
let config = wallet.config().clone();
output_json(&bark_json::cli::Config {
ark: config.server_address,
bitcoind: config.bitcoind_address,
bitcoind_cookie: config.bitcoind_cookiefile.map(|c| c.display().to_string()),
bitcoind_user: config.bitcoind_user,
bitcoind_pass: config.bitcoind_pass,
esplora: config.esplora_address,
vtxo_refresh_expiry_threshold: config.vtxo_refresh_expiry_threshold,
fallback_fee_rate: config.fallback_fee_rate,
})
},
Command::ArkInfo => {
if let Some(info) = wallet.ark_info() {
output_json(&bark_json::cli::ArkInfo::from(info));
} else {
warn!("Could not connect with Ark server.")
}
},
Command::Address { index } => {
if let Some(index) = index {
println!("{}", wallet.peak_address(index)?)
} else {
println!("{}", wallet.new_address()?)
}
},
Command::Balance { no_sync } => {
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
let balance = wallet.balance()?;
output_json(&json::Balance {
spendable: balance.spendable,
pending_in_round: balance.pending_in_round,
pending_lightning_send: balance.pending_lightning_send,
pending_lightning_receive: json::LightningReceiveBalance {
total: balance.pending_lightning_receive.total,
claimable: balance.pending_lightning_receive.claimable,
},
pending_exit: balance.pending_exit,
pending_board: balance.pending_board,
});
},
Command::Vtxos { all, no_sync } => {
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
let mut vtxos = if all {
wallet.all_vtxos()?
} else {
wallet.vtxos()?
};
vtxos.sort_by(|a, b| {
match (a.state.kind(), b.state.kind()) {
(VtxoStateKind::Spent, b) if b != VtxoStateKind::Spent => Ordering::Less,
(VtxoStateKind::Spendable, a) if a != VtxoStateKind::Spendable => Ordering::Greater,
_ => a.expiry_height().cmp(&b.expiry_height()),
}
});
output_json(&vtxos.into_iter().map(WalletVtxoInfo::from).collect::<Vec<_>>());
},
Command::Movements { no_sync } => {
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
let mut movements = wallet.movements()?.into_iter()
.map(json::Movement::try_from)
.collect::<Result<Vec<_>, _>>()?;
movements.sort_by(|l, r| {
let time = l.time.created_at.cmp(&r.time.created_at);
if time == Ordering::Equal {
l.id.inner().cmp(&r.id.inner())
} else {
time
}
});
output_json(&movements);
},
Command::Refresh { vtxos, threshold_blocks, threshold_hours, counterparty, all, no_sync } => {
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
let vtxos = match (threshold_blocks, threshold_hours, counterparty, all, vtxos) {
(None, None, false, false, None) => wallet.get_expiring_vtxos(wallet.config().vtxo_refresh_expiry_threshold).await?,
(Some(b), None, false, false, None) => wallet.get_expiring_vtxos(b).await?,
(None, Some(h), false, false, None) => wallet.get_expiring_vtxos(h*6).await?,
(None, None, true, false, None) => {
let filter = VtxoFilter::new(&wallet).counterparty();
wallet.spendable_vtxos_with(&filter)?
},
(None, None, false, true, None) => wallet.spendable_vtxos()?,
(None, None, false, false, Some(vs)) => {
let vtxos = vs.iter()
.map(|s| {
let id = VtxoId::from_str(s)?;
Ok(wallet.get_vtxo_by_id(id)?)
})
.collect::<anyhow::Result<Vec<_>>>()
.with_context(|| "Invalid vtxo_id")?;
vtxos
}
_ => bail!("please provide either threshold vtxo, threshold_blocks, threshold_hours, counterparty or all"),
};
let vtxos = vtxos.into_iter().map(|v| v.id()).collect::<Vec<_>>();
info!("Refreshing {} vtxos...", vtxos.len());
if let Some(res) = wallet.refresh_vtxos(vtxos).await? {
output_json(&round_status_to_json(&res));
} else {
info!("No round happened");
}
},
Command::Board { amount, all, no_sync } => {
if !no_sync {
info!("Syncing onchain wallet...");
if let Err(e) = onchain.sync(&wallet.chain).await {
warn!("Sync error: {}", e)
}
}
let board = match (amount, all) {
(Some(a), false) => {
info!("Boarding {}...", a);
wallet.board_amount(&mut onchain, a).await?
},
(None, true) => {
info!("Boarding total balance...");
wallet.board_all(&mut onchain).await?
},
_ => bail!("please provide either an amount or --all"),
};
output_json(&json::Board::from(board));
},
Command::Send { destination, amount, comment, no_sync } => {
if let Ok(addr) = ark::Address::from_str(&destination) {
let amount = amount.context("amount missing")?;
if comment.is_some() {
bail!("comment not supported for Ark address");
}
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
info!("Sending arkoor payment of {} to address {}", amount, addr);
wallet.send_arkoor_payment(&addr, amount).await?;
} else if let Ok(inv) = Bolt11Invoice::from_str(&destination) {
pay_invoice(inv, amount, comment, no_sync, &mut wallet).await?;
} else if let Ok(offer) = Offer::from_str(&destination) {
pay_offer(offer, amount, comment, no_sync, &mut wallet).await?;
} else if let Ok(addr) = LightningAddress::from_str(&destination) {
pay_lnaddr(addr, amount, comment, no_sync, &mut wallet).await?;
} else {
bail!("Argument is not a valid destination. Supported are: \
VTXO pubkeys, bolt11 invoices, bolt12 offers and lightning addresses",
);
}
info!("Payment sent succesfully!");
},
Command::SendOnchain { destination, amount, no_sync } => {
if let Ok(addr) = bitcoin::Address::from_str(&destination) {
let addr = addr.require_network(net).with_context(|| {
format!("address is not valid for configured network {}", net)
})?;
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
info!("Sending on-chain payment of {} to {} through round...", amount, addr);
wallet.send_round_onchain_payment(addr, amount).await?;
} else {
bail!("Invalid destination");
}
},
Command::Offboard { address, vtxos , all, no_sync } => {
let address = if let Some(address) = address {
let address = bitcoin::Address::from_str(&address)?
.require_network(net)
.with_context(|| {
format!("address is not valid for configured network {}", net)
})?;
debug!("Sending to on-chain address {}", address);
address
} else {
onchain.address()?
};
let ret = if let Some(vtxos) = vtxos {
let vtxos = vtxos
.into_iter()
.map(|vtxo| {
VtxoId::from_str(&vtxo).with_context(|| format!("invalid vtxoid: {}", vtxo))
})
.collect::<anyhow::Result<Vec<_>>>()?;
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
info!("Offboarding {} vtxos...", vtxos.len());
wallet.offboard_vtxos(vtxos, address).await?
} else if all {
if !no_sync {
info!("Syncing wallet...");
wallet.sync().await;
}
info!("Offboarding all off-chain funds...");
wallet.offboard_all(address).await?
} else {
bail!("Either --vtxos or --all argument must be provided to offboard");
};
output_json(&round_status_to_json(&ret));
},
Command::Onchain(onchain_command) => {
onchain::execute_onchain_command(onchain_command, &mut wallet, &mut onchain).await?;
},
Command::Exit(cmd) => {
exit::execute_exit_command(cmd, &mut wallet, &mut onchain).await?;
},
Command::Lightning(cmd) => {
lightning::execute_lightning_command(cmd, &mut wallet).await?;
},
Command::Maintain => {
wallet.maintenance_with_onchain(&mut onchain).await?;
},
}
Ok(())
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let verbose = cli.verbose;
if let Err(e) = inner_main(cli).await {
eprintln!("An error occurred: {}", e);
if let Some(cause) = e.source() {
eprintln!("Caused by:");
for error in anyhow::Chain::new(cause) {
eprintln!(" {}", error);
}
}
if verbose {
eprintln!();
eprintln!("Stack backtrace:");
eprintln!("{}", e.backtrace());
}
process::exit(1);
}
}