#![cfg_attr(docsrs, cfg_attr(all(), doc = include_str!("../README.md")))]
pub extern crate corepc_client as client;
#[rustfmt::skip]
mod client_versions;
mod versions;
use std::ffi::OsStr;
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::time::Duration;
use std::{env, fmt, fs, thread};
use anyhow::Context;
use corepc_client::client_sync::{self, Auth};
use tempfile::TempDir;
pub use {anyhow, serde_json, tempfile, which};
#[rustfmt::skip] #[doc(inline)]
pub use self::{
client_versions::*,
versions::VERSION,
client::types::model as mtype, };
#[derive(Debug)]
pub struct BitcoinD {
process: Child,
pub client: Client,
work_dir: DataDir,
pub params: ConnectParams,
}
#[derive(Debug)]
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(),
}
}
}
#[derive(Debug, Clone)]
pub struct ConnectParams {
pub cookie_file: PathBuf,
pub rpc_socket: SocketAddrV4,
pub p2p_socket: Option<SocketAddrV4>,
pub zmq_pub_raw_block_socket: Option<SocketAddrV4>,
pub zmq_pub_raw_tx_socket: Option<SocketAddrV4>,
}
pub struct CookieValues {
pub user: String,
pub password: String,
}
impl ConnectParams {
fn parse_cookie(content: String) -> Option<CookieValues> {
let values: Vec<_> = content.splitn(2, ':').collect();
let user = values.first()?.to_string();
let password = values.get(1)?.to_string();
Some(CookieValues { user, password })
}
pub fn get_cookie_values(&self) -> Result<Option<CookieValues>, std::io::Error> {
let cookie = std::fs::read_to_string(&self.cookie_file)?;
Ok(self::ConnectParams::parse_cookie(cookie))
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum P2P {
No,
Yes,
Connect(SocketAddrV4, bool),
}
pub enum Error {
Io(std::io::Error),
Rpc(client_sync::Error),
NoFeature,
NoEnvVar,
NoBitcoindExecutableFound,
EarlyExit(ExitStatus),
BothDirsSpecified,
RpcUserAndPasswordUsed,
SkipDownload,
NoBitcoindInstance(String),
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Error::*;
match self {
Io(_) => write!(f, "io::Error"), Rpc(_) => write!(f, "bitcoin_rpc::Error"),
NoFeature => write!(f, "Called a method requiring a feature to be set, but it's not"),
NoEnvVar => write!(f, "Called a method requiring env var `BITCOIND_EXE` to be set, but it's not"),
NoBitcoindExecutableFound => write!(f, "`bitcoind` executable is required, provide it with one of the following: set env var `BITCOIND_EXE` or use a feature like \"22_1\" or have `bitcoind` executable in the `PATH`"),
EarlyExit(e) => write!(f, "The bitcoind process terminated early with exit code {}", e),
BothDirsSpecified => write!(f, "tempdir and staticdir cannot be enabled at same time in configuration options"),
RpcUserAndPasswordUsed => write!(f, "`-rpcuser` and `-rpcpassword` cannot be used, it will be deprecated soon and it's recommended to use `-rpcauth` instead which works alongside with the default cookie authentication"),
SkipDownload => write!(f, "expecting an auto-downloaded executable but `BITCOIND_SKIP_DOWNLOAD` env var is set"),
NoBitcoindInstance(msg) => write!(f, "it appears that bitcoind is not reachable: {}", msg),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) }
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use Error::*;
match *self {
Error::Io(ref e) => Some(e),
Error::Rpc(ref e) => Some(e),
NoFeature
| NoEnvVar
| NoBitcoindExecutableFound
| EarlyExit(_)
| BothDirsSpecified
| RpcUserAndPasswordUsed
| SkipDownload
| NoBitcoindInstance(_) => None,
}
}
}
const LOCAL_IP: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const INVALID_ARGS: [&str; 2] = ["-rpcuser", "-rpcpassword"];
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Conf<'a> {
pub args: Vec<&'a str>,
pub view_stdout: bool,
pub p2p: P2P,
pub network: &'a str,
pub tmpdir: Option<PathBuf>,
pub staticdir: Option<PathBuf>,
pub attempts: u8,
pub enable_zmq: bool,
pub wallet: Option<String>,
}
impl Default for Conf<'_> {
fn default() -> Self {
Conf {
args: vec!["-regtest", "-fallbackfee=0.0001"],
view_stdout: false,
p2p: P2P::No,
network: "regtest",
tmpdir: None,
staticdir: None,
attempts: 5,
enable_zmq: false,
wallet: Some("default".to_string()),
}
}
}
impl BitcoinD {
pub fn new<S: AsRef<OsStr>>(exe: S) -> anyhow::Result<BitcoinD> {
BitcoinD::with_conf(exe, &Conf::default())
}
pub fn with_conf<S: AsRef<OsStr>>(exe: S, conf: &Conf) -> anyhow::Result<BitcoinD> {
for attempt in 0..conf.attempts {
let work_dir = Self::init_work_dir(conf)?;
let cookie_file = work_dir.path().join(conf.network).join(".cookie");
let rpc_port = get_available_port()?;
let rpc_socket = SocketAddrV4::new(LOCAL_IP, rpc_port);
let rpc_url = format!("http://{}", rpc_socket);
let (p2p_args, p2p_socket) = Self::p2p_args(&conf.p2p)?;
let (zmq_args, zmq_pub_raw_tx_socket, zmq_pub_raw_block_socket) =
Self::zmq_args(conf.enable_zmq)?;
let stdout = if conf.view_stdout { Stdio::inherit() } else { Stdio::null() };
let datadir_arg = format!("-datadir={}", work_dir.path().display());
let rpc_arg = format!("-rpcport={}", rpc_port);
let default_args = [&datadir_arg, &rpc_arg];
let conf_args = validate_args(conf.args.clone())?;
let mut process = Command::new(exe.as_ref())
.args(default_args)
.args(&p2p_args)
.args(&conf_args)
.args(&zmq_args)
.stdout(stdout)
.spawn()
.with_context(|| format!("Error while executing {:?}", exe.as_ref()))?;
match process.try_wait() {
Ok(Some(_)) | Err(_) => {
let _ = process.kill();
continue;
}
Ok(None) => {
}
}
if Self::wait_for_cookie_file(cookie_file.as_path(), Duration::from_secs(5)).is_err() {
let _ = process.kill();
continue;
}
let auth = Auth::CookieFile(cookie_file.clone());
let client_base = Self::create_client_base(&rpc_url, &auth)?;
let client = match &conf.wallet {
Some(wallet) =>
match Self::create_client_wallet(&client_base, &rpc_url, &auth, wallet) {
Ok(client) => client,
Err(e) =>
if attempt == conf.attempts - 1 {
return Err(e);
} else {
let _ = process.kill();
continue;
},
},
None => client_base,
};
if Self::wait_for_client(&client, Duration::from_secs(5)).is_err() {
let _ = process.kill();
continue;
}
return Ok(BitcoinD {
process,
client,
work_dir,
params: ConnectParams {
cookie_file,
rpc_socket,
p2p_socket,
zmq_pub_raw_block_socket,
zmq_pub_raw_tx_socket,
},
});
}
Err(anyhow::anyhow!("Failed to start the node after {} attempts", conf.attempts))
}
fn init_work_dir(conf: &Conf) -> anyhow::Result<DataDir> {
let tmpdir =
conf.tmpdir.clone().or_else(|| env::var("TEMPDIR_ROOT").map(PathBuf::from).ok());
let work_dir = match (&tmpdir, &conf.staticdir) {
(Some(_), Some(_)) => return Err(Error::BothDirsSpecified.into()),
(Some(tmpdir), None) => DataDir::Temporary(TempDir::new_in(tmpdir)?),
(None, Some(workdir)) => {
fs::create_dir_all(workdir)?;
DataDir::Persistent(workdir.to_owned())
}
(None, None) => DataDir::Temporary(TempDir::new()?),
};
Ok(work_dir)
}
fn p2p_args(p2p: &P2P) -> anyhow::Result<(Vec<String>, Option<SocketAddrV4>)> {
match p2p {
P2P::No => Ok((vec!["-listen=0".to_string()], None)),
P2P::Yes => {
let p2p_port = get_available_port()?;
let p2p_socket = SocketAddrV4::new(LOCAL_IP, p2p_port);
let bind_arg = format!("-bind={}", p2p_socket);
let args = vec![bind_arg];
Ok((args, Some(p2p_socket)))
}
P2P::Connect(other_node_url, listen) => {
let p2p_port = get_available_port()?;
let p2p_socket = SocketAddrV4::new(LOCAL_IP, p2p_port);
let bind_arg = format!("-bind={}", p2p_socket);
let connect = format!("-connect={}", other_node_url);
let mut args = vec![bind_arg, connect];
if *listen {
args.push("-listen=1".to_string())
}
Ok((args, Some(p2p_socket)))
}
}
}
fn zmq_args(
enable_zmq: bool,
) -> anyhow::Result<(Vec<String>, Option<SocketAddrV4>, Option<SocketAddrV4>)> {
if enable_zmq {
let zmq_pub_raw_tx_port = get_available_port()?;
let zmq_pub_raw_tx_socket = SocketAddrV4::new(LOCAL_IP, zmq_pub_raw_tx_port);
let zmq_pub_raw_block_port = get_available_port()?;
let zmq_pub_raw_block_socket = SocketAddrV4::new(LOCAL_IP, zmq_pub_raw_block_port);
let zmqpubrawblock_arg =
format!("-zmqpubrawblock=tcp://0.0.0.0:{}", zmq_pub_raw_block_port);
let zmqpubrawtx_arg = format!("-zmqpubrawtx=tcp://0.0.0.0:{}", zmq_pub_raw_tx_port);
Ok((
vec![zmqpubrawtx_arg, zmqpubrawblock_arg],
Some(zmq_pub_raw_tx_socket),
Some(zmq_pub_raw_block_socket),
))
} else {
Ok((vec![], None, None))
}
}
fn wait_for_cookie_file(cookie_file: &Path, timeout: Duration) -> anyhow::Result<()> {
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if cookie_file.exists() {
return Ok(());
}
thread::sleep(Duration::from_millis(200));
}
Err(anyhow::anyhow!("timeout waiting for cookie file: {}", cookie_file.display()))
}
fn wait_for_client(client: &Client, timeout: Duration) -> anyhow::Result<()> {
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if client.call::<serde_json::Value>("getblockchaininfo", &[]).is_ok() {
return Ok(());
}
thread::sleep(Duration::from_millis(200));
}
Err(anyhow::anyhow!("timeout waiting for client to be ready"))
}
fn create_client_base(rpc_url: &str, auth: &Auth) -> anyhow::Result<Client> {
for _ in 0..10 {
if let Ok(client) = Client::new_with_auth(rpc_url, auth.clone()) {
return Ok(client);
}
thread::sleep(Duration::from_millis(200));
}
Client::new_with_auth(rpc_url, auth.clone())
.map_err(|e| Error::NoBitcoindInstance(e.to_string()).into())
}
fn create_client_wallet(
client_base: &Client,
rpc_url: &str,
auth: &Auth,
wallet: &str,
) -> anyhow::Result<Client> {
for _ in 0..10 {
if client_base.create_wallet(wallet).is_ok() || client_base.load_wallet(wallet).is_ok()
{
let url = format!("{}/wallet/{}", rpc_url, wallet);
return Client::new_with_auth(&url, auth.clone())
.map_err(|e| Error::NoBitcoindInstance(e.to_string()).into());
}
thread::sleep(Duration::from_millis(200));
}
Err(Error::NoBitcoindInstance("Could not create or load wallet".to_string()).into())
}
pub fn rpc_url(&self) -> String { format!("http://{}", self.params.rpc_socket) }
pub fn rpc_url_with_wallet<T: AsRef<str>>(&self, wallet_name: T) -> String {
format!("http://{}/wallet/{}", self.params.rpc_socket, wallet_name.as_ref())
}
pub fn workdir(&self) -> PathBuf { self.work_dir.path() }
pub fn p2p_connect(&self, listen: bool) -> Option<P2P> {
self.params.p2p_socket.map(|s| P2P::Connect(s, listen))
}
pub fn stop(&mut self) -> anyhow::Result<ExitStatus> {
self.client.stop()?;
Ok(self.process.wait()?)
}
pub fn create_wallet<T: AsRef<str>>(&self, wallet: T) -> anyhow::Result<Client> {
let _ = self.client.create_wallet(wallet.as_ref())?;
Ok(Client::new_with_auth(
&self.rpc_url_with_wallet(wallet),
Auth::CookieFile(self.params.cookie_file.clone()),
)?)
}
}
#[cfg(feature = "download")]
impl BitcoinD {
pub fn from_downloaded() -> anyhow::Result<BitcoinD> { BitcoinD::new(downloaded_exe_path()?) }
pub fn from_downloaded_with_conf(conf: &Conf) -> anyhow::Result<BitcoinD> {
BitcoinD::with_conf(downloaded_exe_path()?, conf)
}
}
impl Drop for BitcoinD {
fn drop(&mut self) {
if let DataDir::Persistent(_) = self.work_dir {
let _ = self.stop();
}
let _ = self.process.kill();
}
}
pub fn get_available_port() -> anyhow::Result<u16> {
let t = TcpListener::bind(("127.0.0.1", 0))?; Ok(t.local_addr().map(|s| s.port())?)
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self { Error::Io(e) }
}
impl From<client_sync::Error> for Error {
fn from(e: client_sync::Error) -> Self { Error::Rpc(e) }
}
#[cfg(not(feature = "download"))]
pub fn downloaded_exe_path() -> anyhow::Result<String> { Err(Error::NoFeature.into()) }
#[cfg(feature = "download")]
pub fn downloaded_exe_path() -> anyhow::Result<String> {
if std::env::var_os("BITCOIND_SKIP_DOWNLOAD").is_some() {
return Err(Error::SkipDownload.into());
}
let mut path: PathBuf = if let Ok(dir) = std::env::var("BITCOIND_DOWNLOAD_DIR") {
dir.into()
} else {
let mut p: PathBuf = env!("OUT_DIR").into();
p.push("bitcoin");
p
};
path.push(format!("bitcoin-{}", VERSION));
path.push("bin");
if cfg!(target_os = "windows") {
path.push("bitcoind.exe");
} else {
path.push("bitcoind");
}
let path = format!("{}", path.display());
Ok(path)
}
pub fn exe_path() -> anyhow::Result<String> {
if let Ok(path) = std::env::var("BITCOIND_EXE") {
return Ok(path);
}
if let Ok(path) = downloaded_exe_path() {
return Ok(path);
}
which::which("bitcoind")
.map_err(|_| Error::NoBitcoindExecutableFound.into())
.map(|p| p.display().to_string())
}
pub fn validate_args(args: Vec<&str>) -> anyhow::Result<Vec<&str>> {
args.iter().try_for_each(|arg| {
if INVALID_ARGS.iter().any(|x| arg.starts_with(x)) {
return Err(Error::RpcUserAndPasswordUsed);
}
Ok(())
})?;
Ok(args)
}
#[cfg(test)]
mod test {
use std::net::SocketAddrV4;
use tempfile::TempDir;
use super::*;
use crate::{exe_path, get_available_port, BitcoinD, Conf, LOCAL_IP, P2P};
#[test]
fn test_local_ip() {
assert_eq!("127.0.0.1", format!("{}", LOCAL_IP));
let port = get_available_port().unwrap();
let socket = SocketAddrV4::new(LOCAL_IP, port);
assert_eq!(format!("127.0.0.1:{}", port), format!("{}", socket));
}
#[test]
fn test_node_get_blockchain_info() {
let exe = init();
let node = BitcoinD::new(exe).unwrap();
let info = node.client.get_blockchain_info().unwrap();
assert_eq!(0, info.blocks);
}
#[test]
fn test_node() {
let exe = init();
let node = BitcoinD::new(exe).unwrap();
let info = node.client.get_blockchain_info().unwrap();
assert_eq!(0, info.blocks);
let address = node.client.new_address().unwrap();
let _ = node.client.generate_to_address(1, &address).unwrap();
let info = node.client.get_blockchain_info().unwrap();
assert_eq!(1, info.blocks);
}
#[test]
#[cfg(feature = "0_21_2")]
fn test_getindexinfo() {
let exe = init();
let mut conf = Conf::default();
conf.args.push("-txindex");
let node = BitcoinD::with_conf(&exe, &conf).unwrap();
assert!(
node.client.server_version().unwrap() >= 210_000,
"getindexinfo requires bitcoin >0.21"
);
let info: std::collections::HashMap<String, serde_json::Value> =
node.client.call("getindexinfo", &[]).unwrap();
assert!(info.contains_key("txindex"));
assert!(node.client.server_version().unwrap() >= 210_000);
}
#[test]
fn test_p2p() {
let exe = init();
let conf = Conf::<'_> { p2p: P2P::Yes, ..Default::default() };
let node = BitcoinD::with_conf(&exe, &conf).unwrap();
assert_eq!(peers_connected(&node.client), 0);
let other_conf = Conf::<'_> { p2p: node.p2p_connect(false).unwrap(), ..Default::default() };
let other_node = BitcoinD::with_conf(&exe, &other_conf).unwrap();
assert_eq!(peers_connected(&node.client), 1);
assert_eq!(peers_connected(&other_node.client), 1);
}
#[cfg(not(target_os = "windows"))] #[test]
fn test_data_persistence() {
let mut conf = Conf::default();
let datadir = TempDir::new().unwrap();
conf.staticdir = Some(datadir.path().to_path_buf());
let node = BitcoinD::with_conf(exe_path().unwrap(), &conf).unwrap();
let core_addrs = node.client.new_address().unwrap();
node.client.generate_to_address(101, &core_addrs).unwrap();
let wallet_balance_1 = node.client.get_balance().unwrap();
let best_block_1 = node.client.get_best_block_hash().unwrap();
drop(node);
let node = BitcoinD::with_conf(exe_path().unwrap(), &conf).unwrap();
let wallet_balance_2 = node.client.get_balance().unwrap();
let best_block_2 = node.client.get_best_block_hash().unwrap();
assert_eq!(best_block_1, best_block_2);
assert_eq!(wallet_balance_1, wallet_balance_2);
}
#[test]
fn test_multi_p2p() {
let exe = init();
let conf_node1 = Conf::<'_> { p2p: P2P::Yes, ..Default::default() };
let node1 = BitcoinD::with_conf(&exe, &conf_node1).unwrap();
assert_eq!(peers_connected(&node1.client), 0);
let conf_node2 = Conf::<'_> { p2p: node1.p2p_connect(true).unwrap(), ..Default::default() };
let node2 = BitcoinD::with_conf(&exe, &conf_node2).unwrap();
let conf_node3 =
Conf::<'_> { p2p: node2.p2p_connect(false).unwrap(), ..Default::default() };
let node3 = BitcoinD::with_conf(exe_path().unwrap(), &conf_node3).unwrap();
let node1_peers = peers_connected(&node1.client);
let node2_peers = peers_connected(&node2.client);
let node3_peers = peers_connected(&node3.client);
assert!(node1_peers >= 1);
assert!(node2_peers >= 1);
assert_eq!(node3_peers, 1, "listen false but more than 1 peer");
}
#[cfg(feature = "0_19_1")]
#[test]
fn test_multi_wallet() {
use corepc_client::bitcoin::Amount;
let exe = init();
let node = BitcoinD::new(exe).unwrap();
let alice = node.create_wallet("alice").unwrap();
let alice_address = alice.new_address().unwrap();
let bob = node.create_wallet("bob").unwrap();
let bob_address = bob.new_address().unwrap();
node.client.generate_to_address(1, &alice_address).unwrap();
node.client.generate_to_address(101, &bob_address).unwrap();
let balances = alice.get_balances().unwrap();
let alice_balances: vtype::GetBalances = balances;
let balances = bob.get_balances().unwrap();
let bob_balances: vtype::GetBalances = balances;
assert_eq!(
Amount::from_btc(50.0).unwrap(),
Amount::from_btc(alice_balances.mine.trusted).unwrap()
);
assert_eq!(
Amount::from_btc(50.0).unwrap(),
Amount::from_btc(bob_balances.mine.trusted).unwrap()
);
assert_eq!(
Amount::from_btc(5000.0).unwrap(),
Amount::from_btc(bob_balances.mine.immature).unwrap()
);
let _txid = alice.send_to_address(&bob_address, Amount::from_btc(1.0).unwrap()).unwrap();
let balances = alice.get_balances().unwrap();
let alice_balances: vtype::GetBalances = balances;
assert!(
Amount::from_btc(alice_balances.mine.trusted).unwrap()
< Amount::from_btc(49.0).unwrap()
&& Amount::from_btc(alice_balances.mine.trusted).unwrap()
> Amount::from_btc(48.9).unwrap()
);
for _ in 0..30 {
let balances = bob.get_balances().unwrap();
let bob_balances: vtype::GetBalances = balances;
if Amount::from_btc(bob_balances.mine.untrusted_pending).unwrap().to_sat() > 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
let balances = bob.get_balances().unwrap();
let bob_balances: vtype::GetBalances = balances;
assert_eq!(
Amount::from_btc(1.0).unwrap(),
Amount::from_btc(bob_balances.mine.untrusted_pending).unwrap()
);
assert!(node.create_wallet("bob").is_err(), "wallet already exist");
}
#[test]
fn test_node_rpcuser_and_rpcpassword() {
let exe = init();
let mut conf = Conf::default();
conf.args.push("-rpcuser=bitcoind");
conf.args.push("-rpcpassword=bitcoind");
let node = BitcoinD::with_conf(exe, &conf);
assert!(node.is_err());
}
#[test]
fn test_node_rpcauth() {
let exe = init();
let mut conf = Conf::default();
conf.args.push("-rpcauth=bitcoind:cccd5d7fd36e55c1b8576b8077dc1b83$60b5676a09f8518dcb4574838fb86f37700cd690d99bd2fdc2ea2bf2ab80ead6");
let node = BitcoinD::with_conf(exe, &conf).unwrap();
let auth = Auth::UserPass("bitcoind".to_string(), "bitcoind".to_string());
let client = Client::new_with_auth(
format!("{}/wallet/default", node.rpc_url().as_str()).as_str(),
auth,
)
.unwrap();
let info = client.get_blockchain_info().unwrap();
assert_eq!(0, info.blocks);
let address = client.new_address().unwrap();
let _ = client.generate_to_address(1, &address).unwrap();
let info = node.client.get_blockchain_info().unwrap();
assert_eq!(1, info.blocks);
}
#[test]
fn test_get_cookie_user_and_pass() {
let exe = init();
let node = BitcoinD::new(exe).unwrap();
let user: &str = "bitcoind_user";
let password: &str = "bitcoind_password";
std::fs::write(&node.params.cookie_file, format!("{}:{}", user, password)).unwrap();
let result_values = node.params.get_cookie_values().unwrap().unwrap();
assert_eq!(user, result_values.user);
assert_eq!(password, result_values.password);
}
#[test]
fn zmq_interface_enabled() {
let conf = Conf::<'_> { enable_zmq: true, ..Default::default() };
let node = BitcoinD::with_conf(exe_path().unwrap(), &conf).unwrap();
assert!(node.params.zmq_pub_raw_tx_socket.is_some());
assert!(node.params.zmq_pub_raw_block_socket.is_some());
}
#[test]
fn zmq_interface_disabled() {
let exe = init();
let node = BitcoinD::new(exe).unwrap();
assert!(node.params.zmq_pub_raw_tx_socket.is_none());
assert!(node.params.zmq_pub_raw_block_socket.is_none());
}
fn peers_connected(client: &Client) -> usize {
let json = client.get_peer_info().expect("get_peer_info");
json.0.len()
}
fn init() -> String {
let _ = env_logger::try_init();
exe_path().unwrap()
}
}