diff --git a/.gitignore b/.gitignore index cf10b555b5..632b51ef39 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /.vagrant /.vscode /docs/build +/env /fuzz/artifacts /fuzz/corpus /fuzz/coverage diff --git a/Cargo.lock b/Cargo.lock index ae9b1f5fa5..41c2b62bf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2165,6 +2165,7 @@ dependencies = [ "chrono", "ciborium", "clap", + "colored", "criterion", "ctrlc", "dirs", diff --git a/Cargo.toml b/Cargo.toml index 871d538ade..c0c802f6c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ brotli = "3.4.0" chrono = { version = "0.4.19", features = ["serde"] } ciborium = "0.2.1" clap = { version = "4.4.2", features = ["derive"] } +colored = "2.0.4" ctrlc = { version = "3.2.1", features = ["termination"] } dirs = "5.0.0" env_logger = "0.10.0" diff --git a/src/lib.rs b/src/lib.rs index a52138ff02..a79d4672ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ use { mem, net::ToSocketAddrs, path::{Path, PathBuf}, - process, + process::{self, Command, Stdio}, str::FromStr, sync::{ atomic::{self, AtomicBool}, diff --git a/src/options.rs b/src/options.rs index 22c978e46d..11cfe92dbc 100644 --- a/src/options.rs +++ b/src/options.rs @@ -233,14 +233,32 @@ impl Options { } let client = Client::new(&rpc_url, auth) - .with_context(|| format!("failed to connect to Bitcoin Core RPC at {rpc_url}"))?; - - let rpc_chain = match client.get_blockchain_info()?.chain.as_str() { - "main" => Chain::Mainnet, - "test" => Chain::Testnet, - "regtest" => Chain::Regtest, - "signet" => Chain::Signet, - other => bail!("Bitcoin RPC server on unknown chain: {other}"), + .with_context(|| format!("failed to connect to Bitcoin Core RPC at `{rpc_url}`"))?; + + let mut checks = 0; + let rpc_chain = loop { + match client.get_blockchain_info() { + Ok(blockchain_info) => { + break match blockchain_info.chain.as_str() { + "main" => Chain::Mainnet, + "test" => Chain::Testnet, + "regtest" => Chain::Regtest, + "signet" => Chain::Signet, + other => bail!("Bitcoin RPC server on unknown chain: {other}"), + } + } + Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err))) + if err.code == -28 => {} + Err(err) => bail!("Failed to connect to Bitcoin Core RPC at `{rpc_url}`: {err}"), + } + + ensure! { + checks < 100, + "Failed to connect to Bitcoin Core RPC at `{rpc_url}`", + } + + checks += 1; + thread::sleep(Duration::from_millis(100)); }; let ord_chain = self.chain(); diff --git a/src/subcommand.rs b/src/subcommand.rs index 36abd81436..06c105e611 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -2,6 +2,7 @@ use super::*; pub mod balances; pub mod decode; +pub mod env; pub mod epochs; pub mod find; pub mod index; @@ -21,6 +22,8 @@ pub(crate) enum Subcommand { Balances, #[command(about = "Decode a transaction")] Decode(decode::Decode), + #[command(about = "Start a regtest ord and bitcoind instance")] + Env(env::Env), #[command(about = "List the first satoshis of each reward epoch")] Epochs, #[command(about = "Find a satoshi's current location")] @@ -52,6 +55,7 @@ impl Subcommand { match self { Self::Balances => balances::run(options), Self::Decode(decode) => decode.run(options), + Self::Env(env) => env.run(), Self::Epochs => epochs::run(), Self::Find(find) => find.run(options), Self::Index(index) => index.run(options), diff --git a/src/subcommand/env.rs b/src/subcommand/env.rs new file mode 100644 index 0000000000..f1ca3c07cd --- /dev/null +++ b/src/subcommand/env.rs @@ -0,0 +1,131 @@ +use {super::*, colored::Colorize, std::net::TcpListener}; + +struct KillOnDrop(process::Child); + +impl Drop for KillOnDrop { + fn drop(&mut self) { + assert!(Command::new("kill") + .arg(self.0.id().to_string()) + .status() + .unwrap() + .success()); + self.0.wait().unwrap(); + } +} + +#[derive(Debug, Parser)] +pub(crate) struct Env { + #[arg(default_value = "env", help = "Create env in .")] + directory: PathBuf, +} + +impl Env { + pub(crate) fn run(self) -> SubcommandResult { + let (bitcoind_port, ord_port) = ( + TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port(), + TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port(), + ); + + let env = std::env::current_dir()?.join(&self.directory); + + fs::create_dir_all(&env)?; + + let env_string = env + .to_str() + .with_context(|| format!("directory `{}` is not valid unicode", env.display()))?; + + let config = env.join("bitcoin.conf").to_str().unwrap().to_string(); + + fs::write( + env.join("bitcoin.conf"), + format!( + "regtest=1 +datadir={env_string} +listen=0 +txindex=1 +[regtest] +rpcport={bitcoind_port} +", + ), + )?; + + let _bitcoind = KillOnDrop( + Command::new("bitcoind") + .arg(format!("-conf={config}")) + .stdout(Stdio::null()) + .spawn()?, + ); + + loop { + if env.join("regtest/.cookie").try_exists()? { + break; + } + } + + let ord = std::env::current_exe()?; + + let rpc_url = format!("http://localhost:{bitcoind_port}"); + + let _ord = KillOnDrop( + Command::new(&ord) + .arg("--regtest") + .arg("--bitcoin-data-dir") + .arg(&env) + .arg("--data-dir") + .arg(&env) + .arg("--rpc-url") + .arg(&rpc_url) + .arg("server") + .arg("--http-port") + .arg(ord_port.to_string()) + .spawn()?, + ); + + thread::sleep(Duration::from_millis(250)); + + if !env.join("regtest/wallets/ord").try_exists()? { + let status = Command::new(&ord) + .arg("--regtest") + .arg("--bitcoin-data-dir") + .arg(&env) + .arg("--data-dir") + .arg(&env) + .arg("--rpc-url") + .arg(&rpc_url) + .arg("wallet") + .arg("create") + .status()?; + + ensure!(status.success(), "failed to create wallet: {status}"); + } + + let directory = self.directory.to_str().unwrap().to_string(); + + eprintln!( + "{} +bitcoin-cli -datadir='{directory}' getblockchaininfo +{} +{} --regtest --bitcoin-data-dir '{directory}' --data-dir '{directory}' --rpc-url '{}' wallet --server-url http://127.0.0.1:{ord_port} balance", + "Example `bitcoin-cli` command:".blue().bold(), + "Example `ord` command:".blue().bold(), + ord.display(), + rpc_url, + ); + + loop { + if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) { + break Ok(None); + } + + thread::sleep(Duration::from_millis(100)); + } + } +}