#![warn(missing_docs)]
mod error;
mod ext;
mod versions;
use bitcoind::anyhow;
use bitcoind::anyhow::Context;
use bitcoind::bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoind::bitcoincore_rpc::RpcApi;
use bitcoind::tempfile::TempDir;
use bitcoind::{get_available_port, BitcoinD};
use electrum_client::raw_client::{ElectrumPlaintextStream, RawClient};
use log::{debug, error, warn};
use std::env;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
pub use bitcoind;
pub use electrum_client;
pub use error::Error;
#[derive(Debug, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub struct Conf<'a> {
pub args: Vec<&'a str>,
pub view_stderr: bool,
pub http_enabled: bool,
pub network: &'a str,
pub tmpdir: Option<PathBuf>,
pub staticdir: Option<PathBuf>,
attempts: u8,
}
impl Default for Conf<'_> {
fn default() -> Self {
let args = if cfg!(feature = "electrs_0_9_1")
|| cfg!(feature = "electrs_0_8_10")
|| cfg!(feature = "esplora_a33e97e1")
|| cfg!(feature = "legacy")
{
vec!["-vvv"]
} else {
vec![]
};
Conf {
args,
view_stderr: false,
http_enabled: false,
network: "regtest",
tmpdir: None,
staticdir: None,
attempts: 3,
}
}
}
pub struct ElectrsD {
process: Child,
pub client: RawClient<ElectrumPlaintextStream>,
work_dir: DataDir,
pub electrum_url: String,
pub esplora_url: Option<String>,
}
pub enum DataDir {
Persistent(PathBuf),
Temporary(TempDir),
}
impl DataDir {
fn path(&self) -> PathBuf {
match self {
Self::Persistent(path) => path.to_owned(),
Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
}
}
}
impl ElectrsD {
pub fn new<S: AsRef<OsStr>>(exe: S, bitcoind: &BitcoinD) -> anyhow::Result<ElectrsD> {
ElectrsD::with_conf(exe, bitcoind, &Conf::default())
}
pub fn with_conf<S: AsRef<OsStr>>(
exe: S,
bitcoind: &BitcoinD,
conf: &Conf,
) -> anyhow::Result<ElectrsD> {
let response = bitcoind.client.call::<Value>("getblockchaininfo", &[])?;
if response
.get("initialblockdownload")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let node_address = bitcoind.client.call::<Value>("getnewaddress", &[])?;
bitcoind
.client
.call::<Value>("generatetoaddress", &[1.into(), node_address])
.unwrap();
}
let mut args = conf.args.clone();
let work_dir = match (&conf.tmpdir, &conf.staticdir) {
(Some(_), Some(_)) => return Err(Error::BothDirsSpecified.into()),
(Some(tmpdir), None) => DataDir::Temporary(TempDir::new_in(tmpdir)?),
(None, Some(workdir)) => {
std::fs::create_dir_all(workdir)?;
DataDir::Persistent(workdir.to_owned())
}
(None, None) => match env::var("TEMPDIR_ROOT").map(PathBuf::from) {
Ok(path) => DataDir::Temporary(TempDir::new_in(path)?),
Err(_) => DataDir::Temporary(TempDir::new()?),
},
};
let db_dir = format!("{}", work_dir.path().display());
args.push("--db-dir");
args.push(&db_dir);
args.push("--network");
args.push(conf.network);
#[cfg(not(feature = "legacy"))]
let cookie_file;
#[cfg(not(feature = "legacy"))]
{
args.push("--cookie-file");
cookie_file = format!("{}", bitcoind.params.cookie_file.display());
args.push(&cookie_file);
}
#[cfg(feature = "legacy")]
let mut cookie_value;
#[cfg(feature = "legacy")]
{
use std::io::Read;
args.push("--cookie");
let mut cookie = std::fs::File::open(&bitcoind.params.cookie_file)?;
cookie_value = String::new();
cookie.read_to_string(&mut cookie_value)?;
args.push(&cookie_value);
}
args.push("--daemon-rpc-addr");
let rpc_socket = bitcoind.params.rpc_socket.to_string();
args.push(&rpc_socket);
let p2p_socket;
if cfg!(feature = "electrs_0_8_10")
|| cfg!(feature = "esplora_a33e97e1")
|| cfg!(feature = "legacy")
{
args.push("--jsonrpc-import");
} else {
args.push("--daemon-p2p-addr");
p2p_socket = bitcoind
.params
.p2p_socket
.expect("electrs_0_9_1 requires bitcoind with p2p port open")
.to_string();
args.push(&p2p_socket);
}
let electrum_url = format!("0.0.0.0:{}", get_available_port()?);
args.push("--electrum-rpc-addr");
args.push(&electrum_url);
let monitoring = format!("0.0.0.0:{}", get_available_port()?);
args.push("--monitoring-addr");
args.push(&monitoring);
let esplora_url_string;
let esplora_url = if conf.http_enabled {
esplora_url_string = format!("0.0.0.0:{}", get_available_port()?);
args.push("--http-addr");
args.push(&esplora_url_string);
#[allow(clippy::redundant_clone)]
Some(esplora_url_string.clone())
} else {
None
};
let view_stderr = if conf.view_stderr {
Stdio::inherit()
} else {
Stdio::null()
};
debug!("args: {:?}", args);
let mut process = Command::new(&exe)
.args(args)
.stderr(view_stderr)
.spawn()
.with_context(|| format!("Error while executing {:?}", exe.as_ref()))?;
let client = loop {
if let Some(status) = process.try_wait()? {
if conf.attempts > 0 {
warn!("early exit with: {:?}. Trying to launch again ({} attempts remaining), maybe some other process used our available port", status, conf.attempts);
let mut conf = conf.clone();
conf.attempts -= 1;
return Self::with_conf(exe, bitcoind, &conf)
.with_context(|| format!("Remaining attempts {}", conf.attempts));
} else {
error!("early exit with: {:?}", status);
return Err(Error::EarlyExit(status).into());
}
}
match RawClient::new(&electrum_url, None) {
Ok(client) => break client,
Err(_) => std::thread::sleep(Duration::from_millis(500)),
}
};
Ok(ElectrsD {
process,
client,
work_dir,
electrum_url,
esplora_url,
})
}
pub fn trigger(&self) -> anyhow::Result<()> {
Ok(nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.process.id() as i32),
nix::sys::signal::SIGUSR1,
)?)
}
pub fn workdir(&self) -> PathBuf {
self.work_dir.path()
}
pub fn kill(&mut self) -> anyhow::Result<()> {
match self.work_dir {
DataDir::Persistent(_) => {
nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.process.id() as i32),
nix::sys::signal::SIGINT,
)?;
match self.process.wait() {
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
}
DataDir::Temporary(_) => Ok(self.process.kill()?),
}
}
}
impl Drop for ElectrsD {
fn drop(&mut self) {
let _ = self.kill();
}
}
pub fn downloaded_exe_path() -> Option<String> {
if versions::HAS_FEATURE {
Some(format!(
"{}/electrs/{}/electrs",
env!("OUT_DIR"),
versions::electrs_name(),
))
} else {
None
}
}
#[cfg(test)]
mod test {
use crate::bitcoind::P2P;
use crate::ElectrsD;
use bitcoind::bitcoincore_rpc::RpcApi;
use electrum_client::ElectrumApi;
use log::{debug, log_enabled, Level};
use std::env;
#[test]
fn test_electrsd() {
let (electrs_exe, bitcoind, electrsd) = setup_nodes();
let header = electrsd.client.block_headers_subscribe().unwrap();
assert_eq!(header.height, 1);
let address = bitcoind
.client
.get_new_address(None, None)
.unwrap()
.assume_checked();
bitcoind.client.generate_to_address(100, &address).unwrap();
electrsd.trigger().unwrap();
let header = loop {
std::thread::sleep(std::time::Duration::from_millis(100));
let header = electrsd.client.block_headers_subscribe().unwrap();
if header.height > 100 {
break header;
}
};
assert_eq!(header.height, 101);
let electrsd = ElectrsD::new(&electrs_exe, &bitcoind).unwrap();
let header = electrsd.client.block_headers_subscribe().unwrap();
assert_eq!(header.height, 101);
}
#[test]
fn test_kill() {
let (_, bitcoind, mut electrsd) = setup_nodes();
let _ = bitcoind.client.ping().unwrap(); let _ = electrsd.client.ping().unwrap();
assert!(electrsd.client.ping().is_ok());
electrsd.kill().unwrap();
assert!(electrsd.client.ping().is_err());
}
pub(crate) fn setup_nodes() -> (String, bitcoind::BitcoinD, ElectrsD) {
let (bitcoind_exe, electrs_exe) = init();
debug!("bitcoind: {}", &bitcoind_exe);
debug!("electrs: {}", &electrs_exe);
let mut conf = bitcoind::Conf::default();
conf.view_stdout = log_enabled!(Level::Debug);
if !cfg!(feature = "electrs_0_8_10") && !cfg!(feature = "esplora_a33e97e1") {
conf.p2p = P2P::Yes;
}
let bitcoind = bitcoind::BitcoinD::with_conf(&bitcoind_exe, &conf).unwrap();
let electrs_conf = crate::Conf {
view_stderr: log_enabled!(Level::Debug),
..Default::default()
};
let electrsd = ElectrsD::with_conf(&electrs_exe, &bitcoind, &electrs_conf).unwrap();
(electrs_exe, bitcoind, electrsd)
}
fn init() -> (String, String) {
let _ = env_logger::try_init();
let bitcoind_exe_path = bitcoind::exe_path().unwrap();
let electrs_exe_path = if let Ok(env_electrs_exe) = env::var("ELECTRS_EXE") {
env_electrs_exe
} else if let Some(downloaded_exe_path) = crate::downloaded_exe_path() {
downloaded_exe_path
} else {
panic!("when no version feature is specified, you must specify ELECTRS_EXE env var")
};
(bitcoind_exe_path, electrs_exe_path)
}
}