mirror of
https://github.com/romanz/electrs.git
synced 2025-02-24 23:08:39 +01:00
205 lines
6.2 KiB
Rust
205 lines
6.2 KiB
Rust
use anyhow::{Context, Result};
|
|
|
|
use bitcoin::{
|
|
consensus::serialize, hashes::hex::ToHex, Amount, Block, BlockHash, Transaction, Txid,
|
|
};
|
|
use core_rpc::{json, Auth, Client, RpcApi};
|
|
use parking_lot::Mutex;
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::{
|
|
chain::{Chain, NewHeader},
|
|
config::Config,
|
|
p2p::Connection,
|
|
};
|
|
|
|
enum PollResult {
|
|
Done(Result<()>),
|
|
Retry,
|
|
}
|
|
|
|
fn rpc_poll(client: &mut Client) -> PollResult {
|
|
match client.call::<BlockchainInfo>("getblockchaininfo", &[]) {
|
|
Ok(info) => {
|
|
let left_blocks = info.headers - info.blocks;
|
|
if info.initial_block_download || left_blocks > 0 {
|
|
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 {
|
|
info!("waiting for RPC warmup: {}", e.message);
|
|
return PollResult::Retry;
|
|
}
|
|
}
|
|
PollResult::Done(Err(err).context("daemon not available"))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn rpc_connect(config: &Config) -> Result<Client> {
|
|
let rpc_url = format!("http://{}", config.daemon_rpc_addr);
|
|
let auth = config.daemon_auth.get_auth();
|
|
if let Auth::CookieFile(ref path) = auth {
|
|
if !path.exists() {
|
|
bail!("{:?} is missing - is bitcoind running?", path);
|
|
}
|
|
}
|
|
let mut client = Client::new(&rpc_url, auth)
|
|
.with_context(|| format!("failed to connect to RPC: {}", config.daemon_rpc_addr))?;
|
|
|
|
loop {
|
|
match rpc_poll(&mut client) {
|
|
PollResult::Done(result) => return result.map(|()| client),
|
|
PollResult::Retry => {
|
|
std::thread::sleep(std::time::Duration::from_secs(1)); // wait a bit before polling
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Daemon {
|
|
p2p: Mutex<Connection>,
|
|
rpc: Client,
|
|
}
|
|
|
|
// A workaround for https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/190.
|
|
#[derive(Deserialize)]
|
|
struct BlockchainInfo {
|
|
/// The current number of blocks processed in the server
|
|
pub blocks: u64,
|
|
/// The current number of headers we have validated
|
|
pub headers: u64,
|
|
/// Estimate of whether this node is in Initial Block Download mode
|
|
#[serde(rename = "initialblockdownload")]
|
|
pub initial_block_download: bool,
|
|
/// If the blocks are subject to pruning
|
|
pub pruned: bool,
|
|
}
|
|
|
|
impl Daemon {
|
|
pub fn connect(config: &Config) -> Result<Self> {
|
|
let rpc = rpc_connect(config)?;
|
|
let network_info = rpc.get_network_info()?;
|
|
if network_info.version < 21_00_00 {
|
|
bail!("electrs requires bitcoind 0.21+");
|
|
}
|
|
if !network_info.network_active {
|
|
bail!("electrs requires active bitcoind p2p network");
|
|
}
|
|
let blockchain_info: BlockchainInfo = rpc.call("getblockchaininfo", &[])?;
|
|
if blockchain_info.pruned {
|
|
bail!("electrs requires non-pruned bitcoind node");
|
|
}
|
|
let p2p = Mutex::new(Connection::connect(config.network, config.daemon_p2p_addr)?);
|
|
Ok(Self { p2p, rpc })
|
|
}
|
|
|
|
pub(crate) fn estimate_fee(&self, nblocks: u16) -> Result<Option<Amount>> {
|
|
Ok(self
|
|
.rpc
|
|
.estimate_smart_fee(nblocks, None)
|
|
.context("failed to estimate fee")?
|
|
.fee_rate)
|
|
}
|
|
|
|
pub(crate) fn get_relay_fee(&self) -> Result<Amount> {
|
|
Ok(self
|
|
.rpc
|
|
.get_network_info()
|
|
.context("failed to get relay fee")?
|
|
.relay_fee)
|
|
}
|
|
|
|
pub(crate) fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
|
|
self.rpc
|
|
.send_raw_transaction(tx)
|
|
.context("failed to broadcast transaction")
|
|
}
|
|
|
|
pub(crate) fn get_transaction_info(
|
|
&self,
|
|
txid: &Txid,
|
|
blockhash: Option<BlockHash>,
|
|
) -> Result<Value> {
|
|
// 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")
|
|
}
|
|
|
|
pub(crate) fn get_transaction_hex(
|
|
&self,
|
|
txid: &Txid,
|
|
blockhash: Option<BlockHash>,
|
|
) -> Result<Value> {
|
|
let tx = self.get_transaction(txid, blockhash)?;
|
|
Ok(json!(serialize(&tx).to_hex()))
|
|
}
|
|
|
|
pub(crate) fn get_transaction(
|
|
&self,
|
|
txid: &Txid,
|
|
blockhash: Option<BlockHash>,
|
|
) -> Result<Transaction> {
|
|
self.rpc
|
|
.get_raw_transaction(txid, blockhash.as_ref())
|
|
.context("failed to get transaction")
|
|
}
|
|
|
|
pub(crate) fn get_block_txids(&self, blockhash: BlockHash) -> Result<Vec<Txid>> {
|
|
Ok(self
|
|
.rpc
|
|
.get_block_info(&blockhash)
|
|
.context("failed to get block txids")?
|
|
.tx)
|
|
}
|
|
|
|
pub(crate) fn get_mempool_txids(&self) -> Result<Vec<Txid>> {
|
|
self.rpc
|
|
.get_raw_mempool()
|
|
.context("failed to get mempool txids")
|
|
}
|
|
|
|
pub(crate) fn get_mempool_entry(&self, txid: &Txid) -> Result<json::GetMempoolEntryResult> {
|
|
self.rpc
|
|
.get_mempool_entry(txid)
|
|
.context("failed to get mempool entry")
|
|
}
|
|
|
|
pub(crate) fn get_new_headers(&self, chain: &Chain) -> Result<Vec<NewHeader>> {
|
|
self.p2p.lock().get_new_headers(chain)
|
|
}
|
|
|
|
pub(crate) fn for_blocks<B, F>(&self, blockhashes: B, func: F) -> Result<()>
|
|
where
|
|
B: IntoIterator<Item = BlockHash>,
|
|
F: FnMut(BlockHash, Block) + Send,
|
|
{
|
|
self.p2p.lock().for_blocks(blockhashes, func)
|
|
}
|
|
}
|
|
|
|
pub(crate) type RpcError = core_rpc::jsonrpc::error::RpcError;
|
|
|
|
pub(crate) fn extract_bitcoind_error(err: &core_rpc::Error) -> Option<&RpcError> {
|
|
use core_rpc::{jsonrpc::error::Error::Rpc as ServerError, Error::JsonRpc as JsonRpcError};
|
|
match err {
|
|
JsonRpcError(ServerError(e)) => Some(e),
|
|
_ => None,
|
|
}
|
|
}
|