#![warn(missing_docs)]
mod versions;
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;
use std::ffi::OsStr;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
pub use bitcoind;
pub struct Conf<'a> {
pub args: Vec<&'a str>,
pub view_stderr: bool,
pub http_enabled: bool,
pub network: &'a str,
}
impl Default for Conf<'_> {
fn default() -> Self {
Conf {
args: vec!["-vvv"],
view_stderr: false,
http_enabled: false,
network: "regtest",
}
}
}
pub struct ElectrsD {
process: Child,
pub client: RawClient<ElectrumPlaintextStream>,
_db_dir: TempDir,
pub electrum_url: String,
pub esplora_url: Option<String>,
}
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Bitcoind(bitcoind::Error),
ElectrumClient(electrum_client::Error),
BitcoinCoreRpc(bitcoind::bitcoincore_rpc::Error),
#[cfg(feature = "trigger")]
Nix(nix::Error),
}
impl ElectrsD {
pub fn new<S: AsRef<OsStr>>(exe: S, bitcoind: &BitcoinD) -> Result<ElectrsD, Error> {
ElectrsD::with_conf(exe, bitcoind, &Conf::default())
}
pub fn with_conf<S: AsRef<OsStr>>(
exe: S,
bitcoind: &BitcoinD,
conf: &Conf,
) -> Result<ElectrsD, Error> {
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 _db_dir = TempDir::new()?;
let db_dir = format!("{}", _db_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);
args.push("--jsonrpc-import");
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 process = Command::new(exe).args(args).stderr(view_stderr).spawn()?;
let client = loop {
match RawClient::new(&electrum_url, None) {
Ok(client) => break client,
Err(_) => std::thread::sleep(Duration::from_millis(500)),
}
};
Ok(ElectrsD {
process,
client,
_db_dir,
electrum_url,
esplora_url,
})
}
#[cfg(feature = "trigger")]
pub fn trigger(&self) -> Result<(), Error> {
Ok(nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.process.id() as i32),
nix::sys::signal::SIGUSR1,
)?)
}
}
impl Drop for ElectrsD {
fn drop(&mut self) {
let _ = self.process.kill();
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl From<bitcoind::Error> for Error {
fn from(e: bitcoind::Error) -> Self {
Error::Bitcoind(e)
}
}
impl From<electrum_client::Error> for Error {
fn from(e: electrum_client::Error) -> Self {
Error::ElectrumClient(e)
}
}
impl From<bitcoind::bitcoincore_rpc::Error> for Error {
fn from(e: bitcoind::bitcoincore_rpc::Error) -> Self {
Error::BitcoinCoreRpc(e)
}
}
#[cfg(feature = "trigger")]
impl From<nix::Error> for Error {
fn from(e: nix::Error) -> Self {
Error::Nix(e)
}
}
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::ElectrsD;
use bitcoind::bitcoincore_rpc::RpcApi;
use electrum_client::ElectrumApi;
use log::{debug, log_enabled, Level};
use std::env;
#[test]
fn test_electrsd() {
let (bitcoind_exe, electrs_exe) = init();
debug!("bitcoind: {}", &bitcoind_exe);
debug!("electrs: {}", &electrs_exe);
let conf = bitcoind::Conf {
view_stdout: log_enabled!(Level::Debug),
..Default::default()
};
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();
let header = electrsd.client.block_headers_subscribe().unwrap();
assert_eq!(header.height, 1);
let address = bitcoind.client.get_new_address(None, None).unwrap();
bitcoind.client.generate_to_address(100, &address).unwrap();
#[cfg(feature = "trigger")]
electrsd.trigger().unwrap();
let header = loop {
std::thread::sleep(std::time::Duration::from_secs(1));
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);
}
fn init() -> (String, String) {
let _ = env_logger::try_init();
let bitcoind_exe_path = if let Some(downloaded_exe_path) = bitcoind::downloaded_exe_path() {
downloaded_exe_path
} else {
env::var("BITCOIND_EXE").expect(
"when no version feature is specified, you must specify BITCOIND_EXE env var",
)
};
let electrs_exe_path = if let Some(downloaded_exe_path) = crate::downloaded_exe_path() {
downloaded_exe_path
} else {
env::var("ELECTRS_EXE").expect(
"when no version feature is specified, you must specify ELECTRS_EXE env var",
)
};
(bitcoind_exe_path, electrs_exe_path)
}
}