1
2
3use std::borrow::Borrow;
4use std::collections::{HashMap, HashSet};
5use std::str::FromStr as _;
6use std::sync::Arc;
7use std::time::Duration;
8
9use anyhow::Context;
10use bark_runtime::Instant;
11use bdk_core::{BlockId, CheckPoint};
12use bdk_esplora::esplora_client;
13use bitcoin::constants::genesis_block;
14use bitcoin::{
15 Amount, Block, BlockHash, FeeRate, Network, OutPoint, Transaction, Txid, Weight,
16};
17use log::{debug, info, warn};
18use tokio::sync::RwLock;
19
20use bitcoin_ext::{BlockHeight, BlockRef, FeeRateExt, TxStatus};
21use bitcoin_ext::rpc;
22#[cfg(feature = "bitcoind-rpc")]
23use bitcoin_ext::rpc::{
24 BitcoinAsyncRpcExt, BitcoinRpcClient, RPC_INVALID_ADDRESS_OR_KEY,
25 RPC_VERIFY_ALREADY_IN_UTXO_SET,
26};
27#[cfg(feature = "bitcoind-rpc")]
28use bitcoind_async_client::Client as BitcoindClient;
29#[cfg(feature = "bitcoind-rpc")]
30use bitcoind_async_client::error::ClientError as BitcoindClientError;
31#[cfg(feature = "bitcoind-rpc")]
32use bitcoind_async_client::traits::{Broadcaster, Reader};
33
34use crate::daemon::tip_watcher::{TipSource, TipWatcher};
35
36const FEE_RATE_TARGET_CONF_FAST: u16 = 1;
37const FEE_RATE_TARGET_CONF_REGULAR: u16 = 3;
38const FEE_RATE_TARGET_CONF_SLOW: u16 = 6;
39
40const TIP_CACHE_TTL: Duration = Duration::from_secs(1);
45
46const FEE_RATES_CACHE_TTL: Duration = Duration::from_secs(30);
49
50#[cfg(feature = "bitcoind-rpc")]
51const MIN_BITCOIND_VERSION: usize = 290000;
52
53#[derive(Clone, Debug)]
66pub enum ChainSourceSpec {
67 Bitcoind {
68 url: String,
70 auth: rpc::Auth,
72 zmq: Option<String>,
75 },
76 Esplora {
77 url: String,
79 },
80}
81
82impl ChainSourceSpec {
83 pub(crate) fn url(&self) -> &String {
84 match self {
85 ChainSourceSpec::Bitcoind { url, .. } => url,
86 ChainSourceSpec::Esplora { url } => url,
87 }
88 }
89}
90
91pub enum ChainSourceClient {
92 #[cfg(feature = "bitcoind-rpc")]
98 Bitcoind {
99 rpc: BitcoindClient,
100 sync: BitcoinRpcClient,
101 },
102 Esplora(esplora_client::AsyncClient),
103}
104
105impl ChainSourceClient {
106 async fn check_network(&self, expected: Network) -> anyhow::Result<()> {
107 match self {
108 #[cfg(feature = "bitcoind-rpc")]
109 ChainSourceClient::Bitcoind { rpc, .. } => {
110 let network = rpc.network().await?;
111 if expected != network {
112 bail!("Network mismatch: expected {:?}, got {:?}", expected, network);
113 }
114 },
115 ChainSourceClient::Esplora(client) => {
116 let res = client.client().get(format!("{}/block-height/0", client.url()))
117 .send().await?.text().await?;
118 let genesis_hash = BlockHash::from_str(&res)
119 .context("bad response from server (not a blockhash). Esplora client possibly misconfigured")?;
120 if genesis_hash != genesis_block(expected).block_hash() {
121 bail!("Network mismatch: expected {:?}, got {:?}", expected, genesis_hash);
122 }
123 },
124 };
125
126 Ok(())
127 }
128}
129
130pub struct ChainSource {
165 inner: ChainSourceClient,
166 network: Network,
167 zmq_endpoint: Option<String>,
169 fee_rates: RwLock<FeeRates>,
170 fee_rates_fetched_at: RwLock<Option<Instant>>,
173 tip_cache: RwLock<Option<(BlockHeight, Instant)>>,
176}
177
178impl ChainSource {
179 pub async fn require_version(&self) -> anyhow::Result<()> {
184 #[cfg(feature = "bitcoind-rpc")]
185 if let ChainSourceClient::Bitcoind { rpc, .. } = self.inner() {
186 #[derive(Debug, serde::Deserialize)]
187 struct NetworkInfo { version: usize }
188 let info: NetworkInfo = rpc.call_raw("getnetworkinfo", &[]).await?;
189 if info.version < MIN_BITCOIND_VERSION {
190 bail!("Bitcoin Core version is too old, you can participate in rounds but won't be able to unilaterally exit. Please upgrade to 29.0 or higher.");
191 }
192 }
193
194 Ok(())
195 }
196
197 pub(crate) fn inner(&self) -> &ChainSourceClient {
198 &self.inner
199 }
200
201 pub async fn fee_rates(&self) -> FeeRates {
203 self.fee_rates.read().await.clone()
204 }
205
206 pub fn network(&self) -> Network {
208 self.network
209 }
210
211 pub async fn new(
244 spec: ChainSourceSpec,
245 network: Network,
246 fallback_fee: Option<FeeRate>,
247 #[cfg(feature = "socks5-proxy")] proxy: Option<&str>,
248 ) -> anyhow::Result<Self> {
249 let (inner, zmq_endpoint) = match spec {
250 #[cfg(feature = "bitcoind-rpc")]
251 ChainSourceSpec::Bitcoind { url, auth, zmq } => {
252 let sync = BitcoinRpcClient::new(&url, auth.clone())
262 .context("failed to create sync bitcoind rpc client")?;
263 let async_auth = match auth {
264 rpc::Auth::None => bail!(
265 "bitcoind RPC auth is required (cookie file or user/pass)",
266 ),
267 rpc::Auth::UserPass(u, p) => bitcoind_async_client::Auth::UserPass(u, p),
268 rpc::Auth::CookieFile(p) => bitcoind_async_client::Auth::CookieFile(p),
269 };
270 let rpc = BitcoindClient::new(url, async_auth, None, None, None)
271 .context("failed to create async bitcoind rpc client")?;
272 rpc.require_txindex().await?;
273 (ChainSourceClient::Bitcoind { rpc, sync }, zmq)
274 },
275 #[cfg(not(feature = "bitcoind-rpc"))]
276 ChainSourceSpec::Bitcoind { .. } => bail!(
277 "bitcoind RPC backend is not available: this build was compiled without \
278 the `bitcoind-rpc` feature (notably the wasm-web build)",
279 ),
280 ChainSourceSpec::Esplora { url } => (ChainSourceClient::Esplora({
281 let url = crate::utils::url_with_default_https_scheme(&url);
282 let url = url.strip_suffix("/").unwrap_or(&url);
284 #[cfg(feature = "socks5-proxy")]
285 let mut builder = esplora_client::Builder::new(url);
286 #[cfg(not(feature = "socks5-proxy"))]
287 let builder = esplora_client::Builder::new(url);
288 #[cfg(feature = "socks5-proxy")]
289 if let Some(proxy) = proxy {
290 builder = builder.proxy(proxy);
291 }
292 builder.build_async()
293 .with_context(|| format!("failed to create esplora client for url {}", url))?
294 }), None),
295 };
296
297 inner.check_network(network).await?;
298
299 let fee = fallback_fee.unwrap_or(FeeRate::BROADCAST_MIN);
300 let fee_rates = RwLock::new(FeeRates { fast: fee, regular: fee, slow: fee });
301
302 Ok(Self {
303 inner,
304 network,
305 zmq_endpoint,
306 fee_rates,
307 fee_rates_fetched_at: RwLock::new(None),
308 tip_cache: RwLock::new(None),
309 })
310 }
311
312 async fn fetch_fee_rates(&self) -> anyhow::Result<FeeRates> {
313 match self.inner() {
314 #[cfg(feature = "bitcoind-rpc")]
315 ChainSourceClient::Bitcoind { rpc, .. } => {
316 let get_fee_rate = async |target: u16| -> anyhow::Result<FeeRate> {
317 let fee: rpc::json::EstimateSmartFeeResult = rpc.call_raw(
318 "estimatesmartfee",
319 &[
320 target.into(),
321 serde_json::to_value(rpc::json::EstimateMode::Economical)
322 .expect("serializable"),
323 ],
324 ).await?;
325 if let Some(fee_rate) = fee.fee_rate {
326 Ok(FeeRate::from_amount_per_kvb_ceil(fee_rate))
327 } else {
328 Err(anyhow!("No rate returned from estimate_smart_fee for a {} confirmation target", target))
329 }
330 };
331 Ok(FeeRates {
332 fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST).await?,
333 regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR).await.expect("should exist"),
334 slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW).await.expect("should exist"),
335 })
336 },
337 ChainSourceClient::Esplora(client) => {
338 let estimates = client.get_fee_estimates().await?;
340 let get_fee_rate = |target| {
341 let fee = estimates.get(&target).with_context(||
342 format!("No rate returned from get_fee_estimates for a {} confirmation target", target)
343 )?;
344 FeeRate::from_sat_per_vb_decimal_checked_ceil(*fee).with_context(||
345 format!("Invalid rate returned from get_fee_estimates {} for a {} confirmation target", fee, target)
346 )
347 };
348 Ok(FeeRates {
349 fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST)?,
350 regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR)?,
351 slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW)?,
352 })
353 }
354 }
355 }
356
357 pub fn zmq_endpoint(&self) -> Option<&str> {
361 self.zmq_endpoint.as_deref()
362 }
363
364 async fn fetch_tip(&self) -> anyhow::Result<BlockHeight> {
365 match self.inner() {
366 #[cfg(feature = "bitcoind-rpc")]
367 ChainSourceClient::Bitcoind { rpc, .. } => {
368 Ok(rpc.get_block_count().await? as BlockHeight)
369 },
370 ChainSourceClient::Esplora(client) => {
371 Ok(client.get_height().await?)
372 },
373 }
374 }
375
376 pub async fn tip(&self) -> anyhow::Result<BlockHeight> {
377 if let Some((height, fetched_at)) = *self.tip_cache.read().await {
378 if fetched_at.elapsed() < TIP_CACHE_TTL {
379 return Ok(height);
380 }
381 }
382 let height = self.fetch_tip().await?;
383 *self.tip_cache.write().await = Some((height, Instant::now()));
384 Ok(height)
385 }
386
387 pub async fn invalidate_caches(&self) {
392 *self.tip_cache.write().await = None;
393 *self.fee_rates_fetched_at.write().await = None;
394 }
395
396 pub async fn tip_ref(&self) -> anyhow::Result<BlockRef> {
397 self.block_ref(self.tip().await?).await
398 }
399
400 pub(crate) async fn tip_ref_uncached(&self) -> anyhow::Result<BlockRef> {
404 self.block_ref(self.fetch_tip().await?).await
405 }
406
407 pub async fn tip_watcher(
413 self: &Arc<Self>,
414 poll_interval: Duration,
415 ) -> anyhow::Result<TipWatcher> {
416 #[cfg(all(feature = "bitcoind-rpc", not(target_arch = "wasm32")))]
417 if let Some(zmq) = self.zmq_endpoint() {
418 return TipWatcher::start_zmq(self.clone(), zmq, poll_interval).await;
419 }
420 TipWatcher::start_poll(self.clone(), poll_interval).await
421 }
422
423 pub async fn block_ref(&self, height: BlockHeight) -> anyhow::Result<BlockRef> {
424 match self.inner() {
425 #[cfg(feature = "bitcoind-rpc")]
426 ChainSourceClient::Bitcoind { rpc, .. } => {
427 let hash = rpc.get_block_hash(height as u64).await?;
428 Ok(BlockRef { height, hash })
429 },
430 ChainSourceClient::Esplora(client) => {
431 let hash = client.get_block_hash(height).await?;
432 Ok(BlockRef { height, hash })
433 },
434 }
435 }
436
437 pub async fn block(&self, hash: BlockHash) -> anyhow::Result<Option<Block>> {
438 match self.inner() {
439 #[cfg(feature = "bitcoind-rpc")]
440 ChainSourceClient::Bitcoind { rpc, .. } => {
441 match rpc.get_block(&hash).await {
442 Ok(block) => Ok(Some(block)),
443 Err(e) if is_not_found(&e) => Ok(None),
444 Err(e) => Err(e.into()),
445 }
446 },
447 ChainSourceClient::Esplora(client) => {
448 Ok(client.get_block_by_hash(&hash).await?)
449 },
450 }
451 }
452
453 pub async fn mempool_ancestor_info(&self, txid: Txid) -> anyhow::Result<MempoolAncestorInfo> {
456 let mut result = MempoolAncestorInfo::new(txid);
457
458 match self.inner() {
461 #[cfg(feature = "bitcoind-rpc")]
462 ChainSourceClient::Bitcoind { rpc, .. } => {
463 let entry: rpc::json::GetMempoolEntryResult = rpc.call_raw(
464 "getmempoolentry", &[serde_json::to_value(txid).expect("serializable")],
465 ).await?;
466 let err = || anyhow!("missing weight parameter from getmempoolentry");
467
468 result.total_fee = entry.fees.ancestor;
469 result.total_weight = Weight::from_wu(entry.weight.ok_or_else(err)?) +
470 Weight::from_vb(entry.ancestor_size).ok_or_else(err)?;
471 },
472 ChainSourceClient::Esplora(client) => {
473 let status = self.tx_status(txid).await?;
476 if !matches!(status, TxStatus::Mempool) {
477 return Err(anyhow!("{} is not in the mempool, status is {:?}", txid, status));
478 }
479
480 let mut info_map: HashMap<Txid, esplora_client::Tx> = HashMap::new();
481 let mut set = HashSet::from([txid]);
482 while !set.is_empty() {
483 let requests = set.iter().filter_map(|txid| if info_map.contains_key(txid) {
485 None
486 } else {
487 Some((txid, client.get_tx_info(&txid)))
488 }).collect::<Vec<_>>();
489
490 let mut next_set = HashSet::new();
492
493 for (txid, request) in requests {
495 let info = request.await?
496 .ok_or_else(|| anyhow!("unable to retrieve tx info for {}", txid))?;
497 if !info.status.confirmed {
498 for vin in info.vin.iter() {
499 next_set.insert(vin.txid);
500 }
501 }
502 info_map.insert(*txid, info);
503 }
504 set = next_set;
505 }
506 for info in info_map.into_values().filter(|info| !info.status.confirmed) {
508 result.total_fee += info.fee();
509 result.total_weight += info.weight();
510 }
511 },
512 }
513 Ok(result)
515 }
516
517 pub async fn txs_spending_inputs<T: IntoIterator<Item = OutPoint>>(
520 &self,
521 outpoints: T,
522 #[cfg_attr(not(feature = "bitcoind-rpc"), allow(unused_variables))]
523 block_scan_start: BlockHeight,
524 ) -> anyhow::Result<TxsSpendingInputsResult> {
525 let mut res = TxsSpendingInputsResult::new();
526 match self.inner() {
527 #[cfg(feature = "bitcoind-rpc")]
528 ChainSourceClient::Bitcoind { sync, .. } => {
529 let start = block_scan_start.saturating_sub(1);
531 let block_ref = self.block_ref(start).await?;
532 let cp = CheckPoint::new(BlockId {
533 height: block_ref.height,
534 hash: block_ref.hash,
535 });
536
537 debug!("Scanning blocks for spent outpoints with bitcoind, starting at block height {}...", block_scan_start);
538 let outpoint_set = outpoints.into_iter().collect::<HashSet<_>>();
539
540 let sync_client = sync.clone();
543 let cp_for_blocking = cp.clone();
544 res = tokio::task::spawn_blocking(move || -> anyhow::Result<TxsSpendingInputsResult> {
545 let mut res = res;
546 let mut emitter = bdk_bitcoind_rpc::Emitter::new(
547 &sync_client,
548 cp_for_blocking.clone(),
549 cp_for_blocking.height(),
550 bdk_bitcoind_rpc::NO_EXPECTED_MEMPOOL_TXS,
551 );
552 while let Some(em) = emitter.next_block()? {
553 if em.block_height() % 1000 == 0 {
554 info!("Scanned for spent outpoints until block height {}", em.block_height());
555 }
556 for tx in &em.block.txdata {
557 for txin in tx.input.iter() {
558 if outpoint_set.contains(&txin.previous_output) {
559 res.add(
560 txin.previous_output.clone(),
561 tx.compute_txid(),
562 TxStatus::Confirmed(BlockRef {
563 height: em.block_height(),
564 hash: em.block.block_hash().clone(),
565 }),
566 );
567 if res.map.len() == outpoint_set.len() {
568 return Ok(res);
569 }
570 }
571 }
572 }
573 }
574
575 debug!("Finished scanning blocks for spent outpoints, now checking the mempool...");
576 let mempool = emitter.mempool()?;
577 for (tx, _last_seen) in &mempool.update {
578 for txin in tx.input.iter() {
579 if outpoint_set.contains(&txin.previous_output) {
580 res.add(
581 txin.previous_output.clone(),
582 tx.compute_txid(),
583 TxStatus::Mempool,
584 );
585 if res.map.len() == outpoint_set.len() {
586 return Ok(res);
587 }
588 }
589 }
590 }
591 debug!("Finished checking the mempool for spent outpoints");
592 Ok(res)
593 }).await.context("Emitter scan task panicked")??;
594 },
595 ChainSourceClient::Esplora(client) => {
596 for outpoint in outpoints {
597 let output_status = client.get_output_status(&outpoint.txid, outpoint.vout.into()).await?;
598
599 if let Some(output_status) = output_status {
600 if output_status.spent {
601 let tx_status = {
602 let status = output_status.status.expect("Status should be valid if an outpoint is spent");
603 if status.confirmed {
604 TxStatus::Confirmed(BlockRef {
605 height: status.block_height.expect("Confirmed transaction missing block_height"),
606 hash: status.block_hash.expect("Confirmed transaction missing block_hash"),
607 })
608 } else {
609 TxStatus::Mempool
610 }
611 };
612 let txid = output_status.txid.expect("Txid should be valid if an outpoint is spent");
613 res.add(outpoint, txid, tx_status);
614 }
615 }
616 }
617 },
618 }
619
620 Ok(res)
621 }
622
623 pub async fn broadcast_tx(&self, tx: &Transaction) -> anyhow::Result<()> {
624 match self.inner() {
625 #[cfg(feature = "bitcoind-rpc")]
626 ChainSourceClient::Bitcoind { rpc, .. } => {
627 match rpc.send_raw_transaction(tx, None).await {
628 Ok(_) => Ok(()),
629 Err(e) if is_in_utxo_set(&e) => Ok(()),
630 Err(e) => Err(e.into()),
631 }
632 },
633 ChainSourceClient::Esplora(client) => {
634 client.broadcast(tx).await?;
635 Ok(())
636 },
637 }
638 }
639
640 pub async fn broadcast_package(&self, txs: &[impl Borrow<Transaction>]) -> Result<(), BroadcastError> {
641 let package_order = txs.iter()
642 .map(|t| t.borrow().compute_txid())
643 .collect::<Vec<_>>();
644 match self.inner() {
645 #[cfg(feature = "bitcoind-rpc")]
646 ChainSourceClient::Bitcoind { rpc, .. } => {
647 let hexes: Vec<String> = txs.iter()
648 .map(|t| bitcoin::consensus::encode::serialize_hex(t.borrow()))
649 .collect();
650 let res: rpc::SubmitPackageResult = rpc.call_raw("submitpackage", &[hexes.into()])
651 .await
652 .map_err(|e| BroadcastError::Other(e.to_string()))?;
653 if res.package_msg != "success" {
654 return Err(classify_submit_package_errors(
655 &res.package_msg,
656 res.tx_results.values().map(|t| (t.txid, t.error.as_deref())),
657 &package_order,
658 ));
659 }
660 Ok(())
661 },
662 ChainSourceClient::Esplora(client) => {
663 let txs = txs.iter().map(|t| t.borrow().clone()).collect::<Vec<_>>();
664 let res = client.submit_package(&txs, None, None)
665 .await
666 .map_err(|e| BroadcastError::Other(e.to_string()))?;
667 if res.package_msg != "success" {
668 return Err(classify_submit_package_errors(
669 &res.package_msg,
670 res.tx_results.values().map(|t| (t.txid, t.error.as_deref())),
671 &package_order,
672 ));
673 }
674
675 Ok(())
676 },
677 }
678 }
679
680 pub async fn get_tx(&self, txid: &Txid) -> anyhow::Result<Option<Transaction>> {
681 match self.inner() {
682 #[cfg(feature = "bitcoind-rpc")]
683 ChainSourceClient::Bitcoind { rpc, .. } => {
684 match rpc.get_raw_transaction_verbosity_zero(txid).await {
685 Ok(tx) => Ok(Some(tx.0)),
686 Err(e) if is_not_found(&e) => Ok(None),
687 Err(e) => Err(e.into()),
688 }
689 },
690 ChainSourceClient::Esplora(client) => {
691 Ok(client.get_tx(txid).await?)
692 },
693 }
694 }
695
696 pub async fn tx_confirmed(&self, txid: Txid) -> anyhow::Result<Option<BlockHeight>> {
698 Ok(self.tx_status(txid).await?.confirmed_height())
699 }
700
701 pub async fn tx_status(&self, txid: Txid) -> anyhow::Result<TxStatus> {
703 match self.inner() {
704 #[cfg(feature = "bitcoind-rpc")]
705 ChainSourceClient::Bitcoind { rpc, .. } => Ok(bitcoind_tx_status(rpc, txid).await?),
706 ChainSourceClient::Esplora(esplora) => {
707 match esplora.get_tx_info(&txid).await? {
708 Some(info) => match (info.status.block_height, info.status.block_hash) {
709 (Some(block_height), Some(block_hash)) => Ok(TxStatus::Confirmed(BlockRef {
710 height: block_height,
711 hash: block_hash,
712 } )),
713 _ => Ok(TxStatus::Mempool),
714 },
715 None => Ok(TxStatus::NotFound),
716 }
717 },
718 }
719 }
720
721 #[allow(unused)]
722 pub async fn txout_value(&self, outpoint: &OutPoint) -> anyhow::Result<Amount> {
723 let tx = match self.inner() {
724 #[cfg(feature = "bitcoind-rpc")]
725 ChainSourceClient::Bitcoind { rpc, .. } => {
726 rpc.get_raw_transaction_verbosity_zero(&outpoint.txid).await
727 .with_context(|| format!("tx {} unknown", outpoint.txid))?
728 .0
729 },
730 ChainSourceClient::Esplora(client) => {
731 client.get_tx(&outpoint.txid).await?
732 .with_context(|| format!("tx {} unknown", outpoint.txid))?
733 },
734 };
735 Ok(tx.output.get(outpoint.vout as usize).context("outpoint vout out of range")?.value)
736 }
737
738 pub async fn outpoint_spent_confirmed(&self, outpoint: OutPoint) -> anyhow::Result<bool> {
748 match self.inner() {
749 #[cfg(feature = "bitcoind-rpc")]
750 ChainSourceClient::Bitcoind { rpc, .. } => {
751 let utxo = rpc.try_get_tx_out(outpoint, false).await
755 .with_context(|| format!("gettxout {} failed", outpoint))?;
756 Ok(utxo.is_none())
757 },
758 ChainSourceClient::Esplora(client) => {
759 let status = client.get_output_status(&outpoint.txid, outpoint.vout as u64).await
760 .with_context(|| format!("outspend lookup for {} failed", outpoint))?;
761 Ok(status.is_some_and(|s| {
762 s.spent && s.status.is_some_and(|s| s.confirmed)
763 }))
764 },
765 }
766 }
767
768 pub async fn update_fee_rates(&self, fallback_fee: Option<FeeRate>) -> anyhow::Result<()> {
776 if let Some(fetched_at) = *self.fee_rates_fetched_at.read().await {
777 if fetched_at.elapsed() < FEE_RATES_CACHE_TTL {
778 return Ok(());
779 }
780 }
781 let (fee_rates, used_fallback) = match (self.fetch_fee_rates().await, fallback_fee) {
782 (Ok(fee_rates), _) => (fee_rates, false),
783 (Err(e), None) => return Err(e),
784 (Err(e), Some(fallback)) => {
785 warn!("Error getting fee rates, falling back to {} sat/kvB: {}",
786 fallback.to_btc_per_kvb(), e,
787 );
788 (FeeRates { fast: fallback, regular: fallback, slow: fallback }, true)
789 }
790 };
791
792 *self.fee_rates.write().await = fee_rates;
793 if !used_fallback {
794 *self.fee_rates_fetched_at.write().await = Some(Instant::now());
795 }
796 Ok(())
797 }
798}
799
800impl TipSource for ChainSource {
801 async fn tip_ref(&self) -> anyhow::Result<BlockRef> {
802 ChainSource::tip_ref_uncached(self).await
803 }
804}
805
806#[cfg(feature = "bitcoind-rpc")]
812fn is_not_found(e: &BitcoindClientError) -> bool {
813 matches!(e, BitcoindClientError::Server(c, _) if *c == RPC_INVALID_ADDRESS_OR_KEY)
814}
815
816#[cfg(feature = "bitcoind-rpc")]
818fn is_in_utxo_set(e: &BitcoindClientError) -> bool {
819 matches!(e, BitcoindClientError::Server(c, _) if *c == RPC_VERIFY_ALREADY_IN_UTXO_SET)
820}
821
822#[cfg(feature = "bitcoind-rpc")]
825async fn bitcoind_tx_status(
826 rpc: &BitcoindClient, txid: Txid,
827) -> Result<TxStatus, BitcoindClientError> {
828 let res: Result<rpc::GetRawTransactionResult, _> = rpc.call_raw(
829 "getrawtransaction",
830 &[serde_json::to_value(txid).expect("serializable"), true.into()],
831 ).await;
832 let info = match res {
833 Ok(info) => info,
834 Err(e) if is_not_found(&e) => return Ok(TxStatus::NotFound),
835 Err(e) => return Err(e),
836 };
837 let Some(hash) = info.blockhash else {
838 return Ok(TxStatus::Mempool);
839 };
840 let header: rpc::json::GetBlockHeaderResult = rpc.call_raw(
841 "getblockheader",
842 &[serde_json::to_value(hash).expect("serializable"), true.into()],
843 ).await?;
844 if header.confirmations > 0 {
845 Ok(TxStatus::Confirmed(BlockRef {
846 height: header.height as BlockHeight,
847 hash: header.hash,
848 }))
849 } else {
850 Ok(TxStatus::Mempool)
851 }
852}
853
854#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
856pub struct FeeRates {
857 pub fast: FeeRate,
859 pub regular: FeeRate,
861 pub slow: FeeRate,
863}
864
865#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
867pub struct MempoolAncestorInfo {
868 pub txid: Txid,
870 pub total_fee: Amount,
873 pub total_weight: Weight,
875}
876
877impl MempoolAncestorInfo {
878 pub fn new(txid: Txid) -> Self {
879 Self {
880 txid,
881 total_fee: Amount::ZERO,
882 total_weight: Weight::ZERO,
883 }
884 }
885
886 pub fn effective_fee_rate(&self) -> Option<FeeRate> {
887 FeeRate::from_amount_and_weight_ceil(self.total_fee, self.total_weight)
888 }
889}
890
891#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
892pub struct TxsSpendingInputsResult {
893 pub map: HashMap<OutPoint, (Txid, TxStatus)>,
894}
895
896impl TxsSpendingInputsResult {
897 pub fn new() -> Self {
898 Self { map: HashMap::new() }
899 }
900
901 pub fn add(&mut self, outpoint: OutPoint, txid: Txid, status: TxStatus) {
902 self.map.insert(outpoint, (txid, status));
903 }
904
905 pub fn get(&self, outpoint: &OutPoint) -> Option<&(Txid, TxStatus)> {
906 self.map.get(outpoint)
907 }
908
909 pub fn confirmed_txids(&self) -> impl Iterator<Item = (Txid, BlockRef)> + '_ {
910 self.map
911 .iter()
912 .filter_map(|(_, (txid, status))| {
913 match status {
914 TxStatus::Confirmed(block) => Some((*txid, *block)),
915 _ => None,
916 }
917 })
918 }
919
920 pub fn mempool_txids(&self) -> impl Iterator<Item = Txid> + '_ {
921 self.map
922 .iter()
923 .filter(|(_, (_, status))| matches!(status, TxStatus::Mempool))
924 .map(|(_, (txid, _))| *txid)
925 }
926}
927
928#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
935pub enum BroadcastError {
936 #[error("transaction already known to the mempool")]
938 AlreadyKnown,
939 #[error("transaction inputs are missing or already spent")]
941 MissingOrSpentInputs,
942 #[error("insufficient fee, rejecting replacement")]
944 InsufficientReplacementFee,
945 #[error("{0}")]
947 Other(String),
948}
949
950impl BroadcastError {
951 pub fn is_mempool_conflict(&self) -> bool {
954 matches!(
955 self,
956 BroadcastError::AlreadyKnown
957 | BroadcastError::MissingOrSpentInputs
958 | BroadcastError::InsufficientReplacementFee,
959 )
960 }
961}
962
963fn classify_submit_package_errors<'a>(
964 package_msg: &str,
965 tx_results: impl Iterator<Item = (Txid, Option<&'a str>)>,
966 package_order: &[Txid],
967) -> BroadcastError {
968 let mut results: Vec<(Txid, Option<&'a str>)> = tx_results.collect();
974 results.sort_by_key(|(txid, _)| {
975 package_order.iter().position(|t| t == txid).unwrap_or(usize::MAX)
976 });
977
978 let mut saw_already_known = false;
979 let mut root_cause = None;
980 for (_, err) in &results {
981 if let Some(err) = err {
982 if err.contains("txn-already-known") {
983 saw_already_known = true;
985 continue;
986 }
987 root_cause = Some(*err);
988 break;
989 } else {
990 continue;
991 }
992 }
993
994 match root_cause {
995 Some(e) if e.contains("bad-txns-inputs-missingorspent") => {
996 BroadcastError::MissingOrSpentInputs
997 },
998 Some(e) if e.contains("insufficient fee, rejecting replacement") => {
999 BroadcastError::InsufficientReplacementFee
1000 },
1001 Some(_) => {
1002 let combined = results.iter()
1003 .map(|(txid, e)| format!("tx {}: {}", txid, e.unwrap_or("(no error)")))
1004 .collect::<Vec<_>>()
1005 .join(", ");
1006 BroadcastError::Other(format!("msg: '{}', errors: [{}]", package_msg, combined))
1007 },
1008 None if saw_already_known => BroadcastError::AlreadyKnown,
1009 None => BroadcastError::Other(format!("msg: '{}', no tx errors", package_msg)),
1010 }
1011}
1012
1013#[cfg(test)]
1014mod test {
1015 use super::*;
1016 use std::str::FromStr;
1017
1018 #[test]
1019 fn classify_package_errors_attributes_root_cause_in_package_order() {
1020 let parent = Txid::from_str(
1021 "1111111111111111111111111111111111111111111111111111111111111111").unwrap();
1022 let child = Txid::from_str(
1023 "2222222222222222222222222222222222222222222222222222222222222222").unwrap();
1024 let order = [parent, child];
1025
1026 let res = classify_submit_package_errors("transaction failed", [
1029 (parent, None),
1030 (child, Some("bad-txns-inputs-missingorspent")),
1031 ].into_iter(), &order);
1032 assert_eq!(res, BroadcastError::MissingOrSpentInputs);
1033
1034 let res = classify_submit_package_errors("transaction failed", [
1039 (child, Some("bad-txns-inputs-missingorspent")),
1040 (parent, Some("version")),
1041 ].into_iter(), &order);
1042 assert!(matches!(res, BroadcastError::Other(_)), "got {:?}", res);
1043
1044 let res = classify_submit_package_errors("transaction failed", [
1047 (parent, Some("txn-already-known")),
1048 (child, Some("bad-txns-inputs-missingorspent")),
1049 ].into_iter(), &order);
1050 assert_eq!(res, BroadcastError::MissingOrSpentInputs);
1051
1052 let res = classify_submit_package_errors("transaction failed", [
1054 (parent, Some("txn-already-known")),
1055 (child, Some("txn-already-known")),
1056 ].into_iter(), &order);
1057 assert_eq!(res, BroadcastError::AlreadyKnown);
1058
1059 let res = classify_submit_package_errors("transaction failed", [
1061 (parent, None),
1062 (child, Some("insufficient fee, rejecting replacement")),
1063 ].into_iter(), &order);
1064 assert_eq!(res, BroadcastError::InsufficientReplacementFee);
1065
1066 let res = classify_submit_package_errors("package-mempool-limits", [
1068 (parent, None),
1069 (child, None),
1070 ].into_iter(), &order);
1071 assert!(matches!(res, BroadcastError::Other(ref s) if s.contains("package-mempool-limits")));
1072 }
1073}