631 lines
22 KiB
Rust
631 lines
22 KiB
Rust
use base64::Engine;
|
|
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
|
|
use liana::{
|
|
config::BitcoindConfig,
|
|
miniscript::bitcoin::{self, Network},
|
|
random::{random_bytes, RandomnessError},
|
|
};
|
|
use liana_ui::component::form;
|
|
use std::collections::BTreeMap;
|
|
use std::fmt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time;
|
|
|
|
use tracing::{info, warn};
|
|
|
|
#[cfg(target_os = "windows")]
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
|
|
/// Current and previous managed bitcoind versions, in order of descending version.
|
|
pub const VERSIONS: [&str; 3] = ["26.0", "25.1", "25.0"];
|
|
|
|
/// Current managed bitcoind version for new installations.
|
|
pub const VERSION: &str = VERSIONS[0];
|
|
|
|
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
|
pub const SHA256SUM: &str = "6e9864d0f59d5b7e8769ee867dd4b1f91602584b5736796e37d292e5c34d885a";
|
|
|
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
pub const SHA256SUM: &str = "23e5ab226d9e01ffaadef5ffabe8868d0db23db952b90b0593652993680bb8ab";
|
|
|
|
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
|
pub const SHA256SUM: &str = "8d0e909280012d91d08f0321c53a3ceea064682ca635098910b33e4e94c82ed1";
|
|
|
|
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
|
pub fn download_filename() -> String {
|
|
format!("bitcoin-{}-x86_64-apple-darwin.tar.gz", &VERSION)
|
|
}
|
|
|
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
fn download_filename() -> String {
|
|
format!("bitcoin-{}-x86_64-linux-gnu.tar.gz", &VERSION)
|
|
}
|
|
|
|
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
|
fn download_filename() -> String {
|
|
format!("bitcoin-{}-win64.zip", &VERSION)
|
|
}
|
|
|
|
pub fn download_url() -> String {
|
|
format!(
|
|
"https://bitcoincore.org/bin/bitcoin-core-{}/{}",
|
|
&VERSION,
|
|
download_filename()
|
|
)
|
|
}
|
|
|
|
pub fn internal_bitcoind_directory(liana_datadir: &PathBuf) -> PathBuf {
|
|
let mut datadir = PathBuf::from(liana_datadir);
|
|
datadir.push("bitcoind");
|
|
datadir
|
|
}
|
|
|
|
/// Data directory used by internal bitcoind.
|
|
pub fn internal_bitcoind_datadir(liana_datadir: &PathBuf) -> PathBuf {
|
|
let mut datadir = internal_bitcoind_directory(liana_datadir);
|
|
datadir.push("datadir");
|
|
datadir
|
|
}
|
|
|
|
/// Internal bitcoind executable path.
|
|
pub fn internal_bitcoind_exe_path(liana_datadir: &PathBuf, bitcoind_version: &str) -> PathBuf {
|
|
internal_bitcoind_directory(liana_datadir)
|
|
.join(format!("bitcoin-{}", bitcoind_version))
|
|
.join("bin")
|
|
.join(if cfg!(target_os = "windows") {
|
|
"bitcoind.exe"
|
|
} else {
|
|
"bitcoind"
|
|
})
|
|
}
|
|
|
|
/// Path of the `bitcoin.conf` file used by internal bitcoind.
|
|
pub fn internal_bitcoind_config_path(bitcoind_datadir: &PathBuf) -> PathBuf {
|
|
let mut config_path = PathBuf::from(bitcoind_datadir);
|
|
config_path.push("bitcoin.conf");
|
|
config_path
|
|
}
|
|
|
|
/// Path of the cookie file used by internal bitcoind on a given network.
|
|
pub fn internal_bitcoind_cookie_path(bitcoind_datadir: &Path, network: &Network) -> PathBuf {
|
|
let mut cookie_path = bitcoind_datadir.to_path_buf();
|
|
if let Some(dir) = bitcoind_network_dir(network) {
|
|
cookie_path.push(dir);
|
|
}
|
|
cookie_path.push(".cookie");
|
|
cookie_path
|
|
}
|
|
|
|
/// Path of the cookie file used by internal bitcoind on a given network.
|
|
pub fn internal_bitcoind_debug_log_path(lianad_datadir: &PathBuf, network: Network) -> PathBuf {
|
|
let mut debug_log_path = internal_bitcoind_datadir(lianad_datadir);
|
|
if let Some(dir) = bitcoind_network_dir(&network) {
|
|
debug_log_path.push(dir);
|
|
}
|
|
debug_log_path.push("debug.log");
|
|
debug_log_path
|
|
}
|
|
|
|
pub fn bitcoind_network_dir(network: &Network) -> Option<String> {
|
|
let dir = match network {
|
|
Network::Bitcoin => {
|
|
return None;
|
|
}
|
|
Network::Testnet => "testnet3",
|
|
Network::Regtest => "regtest",
|
|
Network::Signet => "signet",
|
|
_ => panic!("Directory required for this network is unknown."),
|
|
};
|
|
Some(dir.to_string())
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub enum RpcAuthParseError {
|
|
MissingColon,
|
|
MissingDollarSign,
|
|
}
|
|
|
|
impl std::fmt::Display for RpcAuthParseError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
Self::MissingColon => write!(
|
|
f,
|
|
"RPC auth string should contain colon between user and salt."
|
|
),
|
|
Self::MissingDollarSign => write!(
|
|
f,
|
|
"RPC auth string should contain dollar sign between salt and password HMAC."
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents RPC auth credentials as stored in bitcoin.conf.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub struct RpcAuth {
|
|
pub user: String,
|
|
salt: String,
|
|
password_hmac: String,
|
|
}
|
|
|
|
impl RpcAuth {
|
|
/// Returns a new `RpcAuth` object for the given `user` with a random salt and password.
|
|
/// This random password is also returned.
|
|
pub fn new(user: &str) -> Result<(Self, String), RandomnessError> {
|
|
// RPC auth generation follows approach in
|
|
// https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py
|
|
let password =
|
|
random_bytes().map(|bytes| base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(bytes))?;
|
|
// As per the Python script, only use 16 bytes for the salt.
|
|
let salt = random_bytes().map(|bytes| hex::encode(&bytes[..16]))?;
|
|
let mut engine = HmacEngine::<sha256::Hash>::new(salt.as_bytes());
|
|
engine.input(password.as_bytes());
|
|
let password_hmac = Hmac::<sha256::Hash>::from_engine(engine);
|
|
|
|
Ok((
|
|
Self {
|
|
user: user.to_string(),
|
|
salt,
|
|
password_hmac: hex::encode(&password_hmac[..]),
|
|
},
|
|
password,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for RpcAuth {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
write!(f, "{}:{}${}", self.user, self.salt, self.password_hmac)
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for RpcAuth {
|
|
type Err = RpcAuthParseError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let (user, salt_pw) = s.split_once(':').ok_or(RpcAuthParseError::MissingColon)?;
|
|
let (salt, pw) = salt_pw
|
|
.split_once('$')
|
|
.ok_or(RpcAuthParseError::MissingDollarSign)?;
|
|
Ok(Self {
|
|
user: user.to_string(),
|
|
salt: salt.to_string(),
|
|
password_hmac: pw.to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Represents section for a single network in `bitcoin.conf` file.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub struct InternalBitcoindNetworkConfig {
|
|
pub rpc_port: u16,
|
|
pub p2p_port: u16,
|
|
pub prune: u32,
|
|
pub rpc_auth: Option<RpcAuth>,
|
|
}
|
|
|
|
/// Represents the `bitcoin.conf` file to be used by internal bitcoind.
|
|
#[derive(Debug, Clone)]
|
|
pub struct InternalBitcoindConfig {
|
|
pub networks: BTreeMap<Network, InternalBitcoindNetworkConfig>,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub enum InternalBitcoindConfigError {
|
|
KeyNotFound(String),
|
|
CouldNotParseValue(String),
|
|
UnexpectedSection(String),
|
|
TooManyElements(String),
|
|
FileNotFound,
|
|
ReadingFile(String),
|
|
WritingFile(String),
|
|
Unexpected(String),
|
|
}
|
|
|
|
impl std::fmt::Display for InternalBitcoindConfigError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
Self::KeyNotFound(e) => write!(f, "Config file does not contain expected key: {}", e),
|
|
Self::CouldNotParseValue(e) => write!(f, "Value could not be parsed: {}", e),
|
|
Self::UnexpectedSection(e) => write!(f, "Unexpected section in file: {}", e),
|
|
Self::TooManyElements(section) => {
|
|
write!(f, "Section in file contains too many elements: {}", section)
|
|
}
|
|
Self::FileNotFound => write!(f, "File not found"),
|
|
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
|
|
Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
|
|
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for InternalBitcoindConfig {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl InternalBitcoindConfig {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
networks: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn from_ini(ini: &ini::Ini) -> Result<Self, InternalBitcoindConfigError> {
|
|
let mut networks = BTreeMap::new();
|
|
for (maybe_sec, prop) in ini {
|
|
if let Some(sec) = maybe_sec {
|
|
let network = Network::from_core_arg(sec)
|
|
.map_err(|e| InternalBitcoindConfigError::UnexpectedSection(e.to_string()))?;
|
|
if prop.len() > 4 {
|
|
return Err(InternalBitcoindConfigError::TooManyElements(
|
|
sec.to_string(),
|
|
));
|
|
}
|
|
let rpc_port = prop
|
|
.get("rpcport")
|
|
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("rpcport".to_string()))?
|
|
.parse::<u16>()
|
|
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
|
|
let p2p_port = prop
|
|
.get("port")
|
|
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("port".to_string()))?
|
|
.parse::<u16>()
|
|
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
|
|
let prune = prop
|
|
.get("prune")
|
|
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))?
|
|
.parse::<u32>()
|
|
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
|
|
let rpc_auth = prop
|
|
.get("rpcauth")
|
|
.map(|v| {
|
|
v.parse::<RpcAuth>().map_err(|e| {
|
|
InternalBitcoindConfigError::CouldNotParseValue(e.to_string())
|
|
})
|
|
})
|
|
.transpose()?;
|
|
|
|
networks.insert(
|
|
network,
|
|
InternalBitcoindNetworkConfig {
|
|
rpc_port,
|
|
p2p_port,
|
|
prune,
|
|
rpc_auth,
|
|
},
|
|
);
|
|
} else if !prop.is_empty() {
|
|
return Err(InternalBitcoindConfigError::UnexpectedSection(
|
|
"General section should be empty".to_string(),
|
|
));
|
|
}
|
|
}
|
|
Ok(Self { networks })
|
|
}
|
|
|
|
pub fn from_file(path: &PathBuf) -> Result<Self, InternalBitcoindConfigError> {
|
|
if !path.exists() {
|
|
return Err(InternalBitcoindConfigError::FileNotFound);
|
|
}
|
|
let conf_ini = ini::Ini::load_from_file(path)
|
|
.map_err(|e| InternalBitcoindConfigError::ReadingFile(e.to_string()))?;
|
|
|
|
Self::from_ini(&conf_ini)
|
|
}
|
|
|
|
pub fn to_ini(&self) -> ini::Ini {
|
|
let mut conf_ini = ini::Ini::new();
|
|
|
|
for (network, network_conf) in &self.networks {
|
|
conf_ini
|
|
.with_section(Some(network.to_core_arg()))
|
|
.set("rpcport", network_conf.rpc_port.to_string())
|
|
.set("port", network_conf.p2p_port.to_string())
|
|
.set("prune", network_conf.prune.to_string());
|
|
if let Some(rpc_auth) = network_conf.rpc_auth.as_ref() {
|
|
conf_ini
|
|
.with_section(Some(network.to_core_arg()))
|
|
.set("rpcauth", rpc_auth.to_string());
|
|
}
|
|
}
|
|
conf_ini
|
|
}
|
|
|
|
pub fn to_file(&self, path: &PathBuf) -> Result<(), InternalBitcoindConfigError> {
|
|
std::fs::create_dir_all(
|
|
path.parent()
|
|
.ok_or_else(|| InternalBitcoindConfigError::Unexpected("No parent".to_string()))?,
|
|
)
|
|
.map_err(|e| InternalBitcoindConfigError::Unexpected(e.to_string()))?;
|
|
info!("Writing to file {}", path.to_string_lossy());
|
|
self.to_ini()
|
|
.write_to_file(path)
|
|
.map_err(|e| InternalBitcoindConfigError::WritingFile(e.to_string()))?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Possible errors when starting bitcoind.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub enum StartInternalBitcoindError {
|
|
CommandError(String),
|
|
CouldNotCanonicalizeDataDir(String),
|
|
BitcoinDError(String),
|
|
ExecutableNotFound,
|
|
ProcessExited(std::process::ExitStatus),
|
|
}
|
|
|
|
impl std::fmt::Display for StartInternalBitcoindError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
Self::CommandError(e) => {
|
|
write!(f, "Command to start bitcoind returned an error: {}", e)
|
|
}
|
|
Self::CouldNotCanonicalizeDataDir(e) => {
|
|
write!(f, "Failed to canonicalize datadir: {}", e)
|
|
}
|
|
Self::BitcoinDError(e) => write!(f, "bitcoind connection check failed: {}", e),
|
|
Self::ExecutableNotFound => write!(f, "bitcoind executable not found."),
|
|
Self::ProcessExited(status) => {
|
|
write!(f, "bitcoind process exited with status '{}'.", status)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub struct Bitcoind {
|
|
_process: Arc<std::process::Child>,
|
|
pub config: BitcoindConfig,
|
|
}
|
|
|
|
impl Bitcoind {
|
|
/// Start internal bitcoind for the given network.
|
|
pub fn start(
|
|
network: &bitcoin::Network,
|
|
config: BitcoindConfig,
|
|
liana_datadir: &PathBuf,
|
|
) -> Result<Self, StartInternalBitcoindError> {
|
|
let bitcoind_datadir = internal_bitcoind_datadir(liana_datadir);
|
|
// Find most recent bitcoind version available.
|
|
let bitcoind_exe_path = VERSIONS
|
|
.iter()
|
|
.filter_map(|v| {
|
|
let path = internal_bitcoind_exe_path(liana_datadir, v);
|
|
if path.exists() {
|
|
Some(path)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.next()
|
|
.ok_or(StartInternalBitcoindError::ExecutableNotFound)?;
|
|
info!(
|
|
"Found bitcoind executable at '{}'.",
|
|
bitcoind_exe_path.to_string_lossy()
|
|
);
|
|
let datadir_path_str = bitcoind_datadir
|
|
.canonicalize()
|
|
.map_err(|e| StartInternalBitcoindError::CouldNotCanonicalizeDataDir(e.to_string()))?
|
|
.to_str()
|
|
.ok_or_else(|| {
|
|
StartInternalBitcoindError::CouldNotCanonicalizeDataDir(
|
|
"Couldn't convert path to str.".to_string(),
|
|
)
|
|
})?
|
|
.to_string();
|
|
|
|
// See https://github.com/rust-lang/rust/issues/42869.
|
|
#[cfg(target_os = "windows")]
|
|
let datadir_path_str = datadir_path_str.replace("\\\\?\\", "").replace("\\\\?", "");
|
|
|
|
let args = vec![
|
|
format!("-chain={}", network.to_core_arg()),
|
|
format!("-datadir={}", datadir_path_str),
|
|
];
|
|
let mut command = std::process::Command::new(bitcoind_exe_path);
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let command = command.creation_flags(CREATE_NO_WINDOW);
|
|
|
|
let mut process = command
|
|
.args(&args)
|
|
// FIXME: can we pipe stderr to our logging system somehow?
|
|
.stdout(std::process::Stdio::null())
|
|
.spawn()
|
|
.map_err(|e| StartInternalBitcoindError::CommandError(e.to_string()))?;
|
|
|
|
// We've started bitcoind in the background, however it may fail to start for whatever
|
|
// reason. And we need its JSONRPC interface to be available to continue. Thus wait for
|
|
// the interface to be created successfully, regularly checking it did not fail to start.
|
|
loop {
|
|
match process.try_wait() {
|
|
Ok(None) => {}
|
|
Err(e) => log::error!("Error while trying to wait for bitcoind: {}", e),
|
|
Ok(Some(status)) => {
|
|
log::error!("Bitcoind exited with status '{}'", status);
|
|
return Err(StartInternalBitcoindError::ProcessExited(status));
|
|
}
|
|
}
|
|
match liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) {
|
|
Ok(_) => {
|
|
log::info!("Bitcoind seems to have successfully started.");
|
|
return Ok(Self {
|
|
config,
|
|
_process: Arc::new(process),
|
|
});
|
|
}
|
|
Err(liana::BitcoindError::CookieFile(_)) => {
|
|
// This is only raised if we're using cookie authentication.
|
|
// Assume cookie file has not been created yet and try again.
|
|
}
|
|
Err(e) => {
|
|
if !e.is_transient() {
|
|
// Non-transient error could happen, e.g., if RPC auth credentials are wrong.
|
|
// Kill process now in case it's not possible to do via RPC command later.
|
|
if let Err(e) = process.kill() {
|
|
log::error!("Error trying to kill bitcoind process: '{}'", e);
|
|
}
|
|
return Err(StartInternalBitcoindError::BitcoinDError(e.to_string()));
|
|
}
|
|
}
|
|
}
|
|
log::info!("Waiting for bitcoind to start.");
|
|
thread::sleep(time::Duration::from_millis(500));
|
|
}
|
|
}
|
|
|
|
/// Stop (internal) bitcoind.
|
|
pub fn stop(&self) {
|
|
stop_bitcoind(&self.config);
|
|
}
|
|
}
|
|
|
|
pub fn stop_bitcoind(config: &BitcoindConfig) -> bool {
|
|
match liana::BitcoinD::new(config, "internal_bitcoind_stop".to_string()) {
|
|
Ok(bitcoind) => {
|
|
info!("Stopping internal bitcoind...");
|
|
bitcoind.stop();
|
|
info!("Stopped liana managed bitcoind");
|
|
true
|
|
}
|
|
Err(e) => {
|
|
warn!("Could not create interface to internal bitcoind: '{}'.", e);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum RpcAuthType {
|
|
CookieFile,
|
|
UserPass,
|
|
}
|
|
|
|
impl fmt::Display for RpcAuthType {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
RpcAuthType::CookieFile => write!(f, "Cookie file path"),
|
|
RpcAuthType::UserPass => write!(f, "User and password"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct RpcAuthValues {
|
|
pub cookie_path: form::Value<String>,
|
|
pub user: form::Value<String>,
|
|
pub password: form::Value<String>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum ConfigField {
|
|
Address,
|
|
CookieFilePath,
|
|
User,
|
|
Password,
|
|
}
|
|
|
|
impl fmt::Display for ConfigField {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
ConfigField::Address => write!(f, "Socket address"),
|
|
ConfigField::CookieFilePath => write!(f, "Cookie file path"),
|
|
ConfigField::User => write!(f, "User"),
|
|
ConfigField::Password => write!(f, "Password"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ini::Ini;
|
|
use liana::miniscript::bitcoin::Network;
|
|
|
|
// Test the format of the internal bitcoind configuration file.
|
|
#[test]
|
|
fn internal_bitcoind_config() {
|
|
// A valid config
|
|
let mut conf_ini = Ini::new();
|
|
conf_ini
|
|
.with_section(Some("main"))
|
|
.set("rpcport", "43345")
|
|
.set("port", "42355")
|
|
.set("prune", "15246");
|
|
conf_ini
|
|
.with_section(Some("regtest"))
|
|
.set("rpcport", "34067")
|
|
.set("port", "45175")
|
|
.set("prune", "2043")
|
|
.set("rpcauth", "my_user:my_salt$my_pw_hmac");
|
|
let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini");
|
|
let main_conf = InternalBitcoindNetworkConfig {
|
|
rpc_port: 43345,
|
|
p2p_port: 42355,
|
|
prune: 15246,
|
|
rpc_auth: None,
|
|
};
|
|
let regtest_conf = InternalBitcoindNetworkConfig {
|
|
rpc_port: 34067,
|
|
p2p_port: 45175,
|
|
prune: 2043,
|
|
rpc_auth: Some(RpcAuth {
|
|
user: "my_user".to_string(),
|
|
salt: "my_salt".to_string(),
|
|
password_hmac: "my_pw_hmac".to_string(),
|
|
}),
|
|
};
|
|
assert_eq!(conf.networks.len(), 2);
|
|
assert_eq!(
|
|
conf.networks.get(&Network::Bitcoin).expect("Missing main"),
|
|
&main_conf
|
|
);
|
|
assert_eq!(
|
|
conf.networks
|
|
.get(&Network::Regtest)
|
|
.expect("Missing regtest"),
|
|
®test_conf
|
|
);
|
|
|
|
let mut conf = InternalBitcoindConfig::new();
|
|
conf.networks.insert(Network::Bitcoin, main_conf);
|
|
conf.networks.insert(Network::Regtest, regtest_conf);
|
|
conf_ini = conf.to_ini();
|
|
assert_eq!(conf_ini.len(), 3); // 2 network sections plus the empty general section
|
|
assert!(conf_ini.general_section().is_empty());
|
|
for (sec, prop) in &conf_ini {
|
|
if let Some(sec) = sec {
|
|
let rpc_port = prop.get("rpcport").expect("rpcport");
|
|
let p2p_port = prop.get("port").expect("port");
|
|
let prune = prop.get("prune").expect("prune");
|
|
let rpc_auth = prop.get("rpcauth");
|
|
if sec == "main" {
|
|
assert_eq!(prop.len(), 3);
|
|
assert_eq!(rpc_port, "43345");
|
|
assert_eq!(p2p_port, "42355");
|
|
assert_eq!(prune, "15246");
|
|
assert!(rpc_auth.is_none());
|
|
} else if sec == "regtest" {
|
|
assert_eq!(prop.len(), 4);
|
|
assert_eq!(rpc_port, "34067");
|
|
assert_eq!(p2p_port, "45175");
|
|
assert_eq!(prune, "2043");
|
|
assert_eq!(rpc_auth, Some("my_user:my_salt$my_pw_hmac"));
|
|
} else {
|
|
panic!("Unexpected section");
|
|
}
|
|
} else {
|
|
assert!(prop.is_empty())
|
|
}
|
|
}
|
|
}
|
|
}
|