use std::fmt;
use std::path::{Path, PathBuf};
use anyhow::Context;
use bitcoin::{FeeRate, Network};
use bitcoin_ext::{BlockDelta, BlockHeight};
use crate::chain::ChainSourceSpec;
use crate::secret::Secret;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BarkNetwork {
Mainnet,
Signet,
Mutinynet,
Regtest,
}
impl BarkNetwork {
pub fn as_bitcoin(&self) -> Network {
match self {
Self::Mainnet => Network::Bitcoin,
Self::Signet => Network::Signet,
Self::Mutinynet => Network::Signet,
Self::Regtest => Network::Regtest,
}
}
}
impl fmt::Display for BarkNetwork {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Mainnet => f.write_str("mainnet"),
Self::Signet => f.write_str("signet"),
Self::Mutinynet => f.write_str("mutinynet"),
Self::Regtest => f.write_str("regtest"),
}
}
}
impl fmt::Debug for BarkNetwork {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
pub const DEFAULT_VTXO_KEY_GAP_LIMIT: u32 = 250;
pub const MAX_VTXO_KEY_GAP_LIMIT: u32 = 100_000;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub server_address: String,
#[deprecated(
since = "0.2.4",
note = "access tokens are not enforced by the server; this field will be removed",
)]
pub server_access_token: Option<String>,
pub user_agent: Option<String>,
pub esplora_address: Option<String>,
pub bitcoind_address: Option<String>,
pub bitcoind_cookiefile: Option<PathBuf>,
pub bitcoind_user: Option<String>,
pub bitcoind_pass: Option<Secret<String>>,
pub bitcoind_zmq_address: Option<String>,
pub vtxo_refresh_expiry_threshold: BlockDelta,
pub vtxo_exit_margin: BlockDelta,
pub htlc_recv_claim_delta: BlockDelta,
pub lightning_receive_claim_retries: u8,
#[cfg(feature = "socks5-proxy")]
pub socks5_proxy: Option<String>,
pub fallback_fee_rate: Option<FeeRate>,
pub round_tx_required_confirmations: BlockHeight,
pub offboard_required_confirmations: BlockHeight,
pub offboard_lost_tx_grace_period_secs: u64,
pub daemon_sync_interval_secs: u64,
pub change_vtxo_split_factor: u8,
pub daemon_manual_sync: bool,
pub vtxo_key_gap_limit: u32,
}
impl Config {
pub fn network_default(network: Network) -> Self {
#[allow(deprecated)]
let mut ret = Self {
server_address: "http://127.0.0.1:3535".to_owned(),
server_access_token: None,
user_agent: None,
esplora_address: None,
bitcoind_address: None,
bitcoind_cookiefile: None,
bitcoind_user: None,
bitcoind_pass: None,
bitcoind_zmq_address: None,
#[cfg(feature = "socks5-proxy")]
socks5_proxy: None,
vtxo_refresh_expiry_threshold: 144,
vtxo_exit_margin: 12,
htlc_recv_claim_delta: 18,
lightning_receive_claim_retries: 5,
fallback_fee_rate: Some(FeeRate::from_sat_per_vb_u32(2)),
round_tx_required_confirmations: 1,
offboard_required_confirmations: 2,
offboard_lost_tx_grace_period_secs: 3600,
daemon_sync_interval_secs: 60,
daemon_manual_sync: false,
change_vtxo_split_factor: 2,
vtxo_key_gap_limit: DEFAULT_VTXO_KEY_GAP_LIMIT,
};
if network != Network::Bitcoin {
ret.vtxo_refresh_expiry_threshold = 12;
ret.fallback_fee_rate = Some(FeeRate::from_sat_per_vb_u32(1));
ret.round_tx_required_confirmations = 1;
ret.offboard_required_confirmations = 0;
}
ret
}
pub fn load(network: Network, path: impl AsRef<Path>) -> anyhow::Result<Config> {
let default = config::Config::try_from(&Self::network_default(network))
.expect("default config failed to deconstruct");
let config = config::Config::builder()
.add_source(default)
.add_source(config::File::from(path.as_ref()).required(false))
.add_source(config::Environment::with_prefix("BARK"))
.build().context("error building config")?
.try_deserialize::<Config>().context("error parsing config")?;
if config.vtxo_key_gap_limit > MAX_VTXO_KEY_GAP_LIMIT {
bail!("vtxo_key_gap_limit {} is above the maximum of {}",
config.vtxo_key_gap_limit, MAX_VTXO_KEY_GAP_LIMIT);
}
Ok(config)
}
pub fn chain_source(&self) -> anyhow::Result<ChainSourceSpec> {
if let Some(ref url) = self.esplora_address {
Ok(ChainSourceSpec::Esplora {
url: url.clone(),
})
} else if let Some(ref url) = self.bitcoind_address {
let auth = if let Some(ref c) = self.bitcoind_cookiefile {
bitcoin_ext::rpc::Auth::CookieFile(c.clone())
} else {
bitcoin_ext::rpc::Auth::UserPass(
self.bitcoind_user.clone().context("need bitcoind auth config")?,
self.bitcoind_pass.as_ref().context("need bitcoind auth config")?
.leak_ref().clone(),
)
};
Ok(ChainSourceSpec::Bitcoind {
url: url.clone(),
auth,
zmq: self.bitcoind_zmq_address.clone(),
})
} else {
bail!("Need to either provide esplora or bitcoind info");
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn config_file_without_gap_limit_gets_the_default() {
let dir = std::env::temp_dir().join(format!("bark-cfg-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
std::fs::write(&path, "\
server_address = \"http://127.0.0.1:3535\"\n\
esplora_address = \"http://127.0.0.1:3002\"\n\
").unwrap();
let config = Config::load(Network::Regtest, &path).expect("an old config should load");
assert_eq!(config.vtxo_key_gap_limit, DEFAULT_VTXO_KEY_GAP_LIMIT,
"a missing gap limit should fall back to the default");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn config_file_with_an_out_of_range_gap_limit_is_refused() {
let dir = std::env::temp_dir().join(format!("bark-cfg-max-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
let write = |limit: u32| std::fs::write(&path, format!("\
server_address = \"http://127.0.0.1:3535\"\n\
esplora_address = \"http://127.0.0.1:3002\"\n\
vtxo_key_gap_limit = {limit}\n\
")).unwrap();
write(MAX_VTXO_KEY_GAP_LIMIT + 1);
let err = Config::load(Network::Regtest, &path)
.expect_err("a gap limit above the maximum should be refused");
assert!(err.to_string().contains("above the maximum"), "err: {err:#}");
write(MAX_VTXO_KEY_GAP_LIMIT);
let config = Config::load(Network::Regtest, &path).expect("the maximum should load");
assert_eq!(config.vtxo_key_gap_limit, MAX_VTXO_KEY_GAP_LIMIT);
std::fs::remove_dir_all(&dir).ok();
}
}