use anyhow::{Context, Error, Result}; use bitcoincore_rpc::json::{ CreateRawTransactionInput, ListUnspentQueryOptions, ListUnspentResultEntry, WalletCreateFundedPsbtOptions, }; use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi}; use sdk_common::sp_client::bitcoin::bip158::BlockFilter; use sdk_common::sp_client::bitcoin::{ block, Address, Amount, Block, BlockHash, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, }; use sdk_common::sp_client::bitcoin::{consensus::deserialize, hashes::hex::FromHex}; // use crossbeam_channel::Receiver; // use parking_lot::Mutex; use serde_json::{json, Value}; use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; use crate::FAUCET_AMT; pub struct SensitiveAuth(pub Auth); impl SensitiveAuth { pub(crate) fn get_auth(&self) -> Auth { self.0.clone() } } enum PollResult { Done(Result<()>), Retry, } fn rpc_poll(client: &mut Client, skip_block_download_wait: bool) -> PollResult { match client.get_blockchain_info() { Ok(info) => { if skip_block_download_wait { // bitcoind RPC is available, don't wait for block download to finish return PollResult::Done(Ok(())); } let left_blocks = info.headers - info.blocks; if info.initial_block_download || left_blocks > 0 { log::info!( "waiting for {} blocks to download{}", left_blocks, if info.initial_block_download { " (IBD)" } else { "" } ); return PollResult::Retry; } PollResult::Done(Ok(())) } Err(err) => { if let Some(e) = extract_bitcoind_error(&err) { if e.code == -28 { log::debug!("waiting for RPC warmup: {}", e.message); return PollResult::Retry; } } PollResult::Done(Err(err).context("daemon not available")) } } } fn read_cookie(path: &Path) -> Result<(String, String)> { // Load username and password from bitcoind cookie file: // * https://github.com/bitcoin/bitcoin/pull/6388/commits/71cbeaad9a929ba6a7b62d9b37a09b214ae00c1a // * https://bitcoin.stackexchange.com/questions/46782/rpc-cookie-authentication let mut file = File::open(path) .with_context(|| format!("failed to open bitcoind cookie file: {}", path.display()))?; let mut contents = String::new(); file.read_to_string(&mut contents) .with_context(|| format!("failed to read bitcoind cookie from {}", path.display()))?; let parts: Vec<&str> = contents.splitn(2, ':').collect(); anyhow::ensure!( parts.len() == 2, "failed to parse bitcoind cookie - missing ':' separator" ); Ok((parts[0].to_owned(), parts[1].to_owned())) } fn rpc_connect(rpcwallet: Option, network: Network, mut rpc_url: String, cookie_path: Option) -> Result { match rpcwallet { Some(ref rpcwallet) => { rpc_url.push_str("/wallet/"); rpc_url.push_str(rpcwallet); }, None => (), } log::info!("Attempting to connect to Bitcoin Core at: {}", rpc_url); // Allow `wait_for_new_block` to take a bit longer before timing out. // See https://github.com/romanz/electrs/issues/495 for more details. let builder = jsonrpc::simple_http::SimpleHttpTransport::builder() .url(&rpc_url)? .timeout(Duration::from_secs(30)); // Prefer explicit user/pass via environment variables if provided let rpc_user_env = env::var("RELAY_RPC_USER").ok(); let rpc_pass_env = env::var("RELAY_RPC_PASSWORD").ok(); let daemon_auth = if let (Some(u), Some(p)) = (rpc_user_env, rpc_pass_env) { SensitiveAuth(Auth::UserPass(u, p)) } else { let cookie_path = match cookie_path { Some(path) => path, None => { // Fallback to default path let home = env::var("HOME")?; let mut default_path = PathBuf::from_str(&home)?; default_path.push(".bitcoin"); default_path.push(network.to_core_arg()); default_path.push(".cookie"); default_path } }; SensitiveAuth(Auth::CookieFile(cookie_path)) }; let builder = match daemon_auth.get_auth() { Auth::None => builder, Auth::UserPass(user, pass) => builder.auth(user, Some(pass)), Auth::CookieFile(path) => { let (user, pass) = read_cookie(&path)?; builder.auth(user, Some(pass)) } }; Ok(Client::from_jsonrpc(jsonrpc::Client::with_transport( builder.build(), ))) } #[derive(Debug)] pub struct Daemon { rpc: Client, } impl RpcCall for Daemon { fn connect(rpcwallet: Option, rpc_url: String, network: Network, cookie_path: Option) -> Result { let mut rpc = rpc_connect(rpcwallet, network, rpc_url, cookie_path)?; loop { match rpc_poll(&mut rpc, false) { PollResult::Done(result) => { result.context("bitcoind RPC polling failed")?; break; // on success, finish polling } PollResult::Retry => { std::thread::sleep(std::time::Duration::from_secs(1)); // wait a bit before polling } } } let network_info = rpc.get_network_info()?; if !network_info.network_active { anyhow::bail!("electrs requires active bitcoind p2p network"); } let info = rpc.get_blockchain_info()?; if info.pruned { anyhow::bail!("electrs requires non-pruned bitcoind node"); } Ok(Self { rpc }) } fn estimate_fee(&self, nblocks: u16) -> Result { let res = self .rpc .estimate_smart_fee(nblocks, None) .context("failed to estimate fee")?; if res.errors.is_some() { Err(Error::msg(serde_json::to_string(&res.errors.unwrap())?)) } else { Ok(res.fee_rate.unwrap()) } } fn get_relay_fee(&self) -> Result { Ok(self .rpc .get_network_info() .context("failed to get relay fee")? .relay_fee) } fn get_current_height(&self) -> Result { Ok(self .rpc .get_block_count() .context("failed to get block count")?) } fn get_block(&self, block_hash: BlockHash) -> Result { Ok(self .rpc .get_block(&block_hash) .context("failed to get block")?) } fn get_filters(&self, block_height: u32) -> Result<(u32, BlockHash, BlockFilter)> { let block_hash = self.rpc.get_block_hash(block_height.try_into()?)?; let filter = self .rpc .get_block_filter(&block_hash) .context("failed to get block filter")? .into_filter(); Ok((block_height, block_hash, filter)) } fn list_unspent_from_to( &self, minamt: Option, ) -> Result> { let minimum_sum_amount = if minamt.is_none() || minamt <= FAUCET_AMT.checked_mul(2) { FAUCET_AMT.checked_mul(2) } else { minamt }; Ok(self.rpc.list_unspent( None, None, None, Some(true), Some(ListUnspentQueryOptions { minimum_sum_amount, ..Default::default() }), )?) } fn create_psbt( &self, unspents: &[ListUnspentResultEntry], spk: ScriptBuf, network: Network, ) -> Result { let inputs: Vec = unspents .iter() .map(|utxo| CreateRawTransactionInput { txid: utxo.txid, vout: utxo.vout, sequence: None, }) .collect(); let address = Address::from_script(&spk, network)?; let total_amt = unspents .iter() .fold(Amount::from_sat(0), |acc, x| acc + x.amount); if total_amt < FAUCET_AMT { return Err(Error::msg("Not enought funds")); } let mut outputs = HashMap::new(); outputs.insert(address.to_string(), total_amt); let options = WalletCreateFundedPsbtOptions { subtract_fee_from_outputs: vec![0], ..Default::default() }; let wallet_create_funded_result = self.rpc .wallet_create_funded_psbt(&inputs, &outputs, None, Some(options), None)?; Ok(wallet_create_funded_result.psbt.to_string()) } fn process_psbt(&self, psbt: String) -> Result { let processed_psbt = self.rpc.wallet_process_psbt(&psbt, None, None, None)?; match processed_psbt.complete { true => Ok(processed_psbt.psbt), false => Err(Error::msg("Failed to complete the psbt")), } } fn finalize_psbt(&self, psbt: String) -> Result { let final_tx = self.rpc.finalize_psbt(&psbt, Some(false))?; match final_tx.complete { true => Ok(final_tx .psbt .expect("We shouldn't have an empty psbt for a complete return")), false => Err(Error::msg("Failed to finalize psbt")), } } fn get_network(&self) -> Result { let blockchain_info = self.rpc.get_blockchain_info()?; Ok(blockchain_info.chain) } fn test_mempool_accept( &self, tx: &Transaction, ) -> Result { let res = self.rpc.test_mempool_accept(&vec![tx])?; Ok(res.get(0).unwrap().clone()) } fn broadcast(&self, tx: &Transaction) -> Result { let txid = self.rpc.send_raw_transaction(tx)?; Ok(txid) } fn get_transaction_info(&self, txid: &Txid, blockhash: Option) -> Result { // No need to parse the resulting JSON, just return it as-is to the client. self.rpc .call( "getrawtransaction", &[json!(txid), json!(true), json!(blockhash)], ) .context("failed to get transaction info") } fn get_transaction_hex(&self, txid: &Txid, blockhash: Option) -> Result { use sdk_common::sp_client::bitcoin::consensus::serde::{hex::Lower, Hex, With}; let tx = self.get_transaction(txid, blockhash)?; #[derive(serde::Serialize)] #[serde(transparent)] struct TxAsHex(#[serde(with = "With::>")] Transaction); serde_json::to_value(TxAsHex(tx)).map_err(Into::into) } fn get_transaction(&self, txid: &Txid, blockhash: Option) -> Result { self.rpc .get_raw_transaction(txid, blockhash.as_ref()) .context("failed to get transaction") } fn get_block_txids(&self, blockhash: BlockHash) -> Result> { Ok(self .rpc .get_block_info(&blockhash) .context("failed to get block txids")? .tx) } fn get_mempool_txids(&self) -> Result> { self.rpc .get_raw_mempool() .context("failed to get mempool txids") } fn get_mempool_entries( &self, txids: &[Txid], ) -> Result>> { let client = self.rpc.get_jsonrpc_client(); log::debug!("getting {} mempool entries", txids.len()); let args: Vec<_> = txids .iter() .map(|txid| vec![serde_json::value::to_raw_value(txid).unwrap()]) .collect(); let reqs: Vec<_> = args .iter() .map(|a| client.build_request("getmempoolentry", a)) .collect(); let res = client.send_batch(&reqs).context("batch request failed")?; log::debug!("got {} mempool entries", res.len()); Ok(res .into_iter() .map(|r| { r.context("missing response")? .result::() .context("invalid response") }) .collect()) } fn get_mempool_transactions(&self, txids: &[Txid]) -> Result>> { let client = self.rpc.get_jsonrpc_client(); log::debug!("getting {} transactions", txids.len()); let args: Vec<_> = txids .iter() .map(|txid| vec![serde_json::value::to_raw_value(txid).unwrap()]) .collect(); let reqs: Vec<_> = args .iter() .map(|a| client.build_request("getrawtransaction", a)) .collect(); let res = client.send_batch(&reqs).context("batch request failed")?; log::debug!("got {} mempool transactions", res.len()); Ok(res .into_iter() .map(|r| -> Result { let tx_hex = r .context("missing response")? .result::() .context("invalid response")?; let tx_bytes = Vec::from_hex(&tx_hex).context("non-hex transaction")?; deserialize(&tx_bytes).context("invalid transaction") }) .collect()) } } pub(crate) trait RpcCall: Send + Sync + std::fmt::Debug { fn connect(rpcwallet: Option, rpc_url: String, network: Network, cookie_path: Option) -> Result where Self: Sized; fn estimate_fee(&self, nblocks: u16) -> Result; fn get_relay_fee(&self) -> Result; fn get_current_height(&self) -> Result; fn get_block(&self, block_hash: BlockHash) -> Result; fn get_filters(&self, block_height: u32) -> Result<(u32, BlockHash, BlockFilter)>; fn list_unspent_from_to( &self, minamt: Option, ) -> Result>; fn create_psbt( &self, unspents: &[ListUnspentResultEntry], spk: ScriptBuf, network: Network, ) -> Result; fn process_psbt(&self, psbt: String) -> Result; fn finalize_psbt(&self, psbt: String) -> Result; fn get_network(&self) -> Result; fn test_mempool_accept( &self, tx: &Transaction, ) -> Result; fn broadcast(&self, tx: &Transaction) -> Result; fn get_transaction_info(&self, txid: &Txid, blockhash: Option) -> Result; fn get_transaction_hex(&self, txid: &Txid, blockhash: Option) -> Result; fn get_transaction(&self, txid: &Txid, blockhash: Option) -> Result; fn get_block_txids(&self, blockhash: BlockHash) -> Result>; fn get_mempool_txids(&self) -> Result>; fn get_mempool_entries( &self, txids: &[Txid], ) -> Result>>; fn get_mempool_transactions(&self, txids: &[Txid]) -> Result>>; } pub(crate) type RpcError = bitcoincore_rpc::jsonrpc::error::RpcError; pub(crate) fn extract_bitcoind_error(err: &bitcoincore_rpc::Error) -> Option<&RpcError> { use bitcoincore_rpc::{ jsonrpc::error::Error::Rpc as ServerError, Error::JsonRpc as JsonRpcError, }; match err { JsonRpcError(ServerError(e)) => Some(e), _ => None, } }