1
2
3use std::borrow::Borrow;
4use std::collections::{HashMap, HashSet};
5use std::str::FromStr as _;
6
7use anyhow::Context;
8use bdk_core::{BlockId, CheckPoint};
9use bdk_esplora::esplora_client;
10use bitcoin::constants::genesis_block;
11use bitcoin::{
12 Amount, Block, BlockHash, FeeRate, Network, OutPoint, Transaction, Txid, Weight,
13};
14use log::{debug, info, warn};
15use tokio::sync::RwLock;
16
17use bitcoin_ext::{BlockHeight, BlockRef, FeeRateExt, TxStatus};
18use bitcoin_ext::rpc;
19#[cfg(feature = "bitcoind-rpc")]
20use bitcoin_ext::rpc::{
21 BitcoinRpcClient, RPC_INVALID_ADDRESS_OR_KEY, RPC_VERIFY_ALREADY_IN_UTXO_SET,
22};
23#[cfg(feature = "bitcoind-rpc")]
24use bitcoind_async_client::Client as BitcoindClient;
25#[cfg(feature = "bitcoind-rpc")]
26use bitcoind_async_client::error::ClientError as BitcoindClientError;
27#[cfg(feature = "bitcoind-rpc")]
28use bitcoind_async_client::traits::{Broadcaster, Reader};
29
30const FEE_RATE_TARGET_CONF_FAST: u16 = 1;
31const FEE_RATE_TARGET_CONF_REGULAR: u16 = 3;
32const FEE_RATE_TARGET_CONF_SLOW: u16 = 6;
33
34#[cfg(feature = "bitcoind-rpc")]
35const MIN_BITCOIND_VERSION: usize = 290000;
36
37#[derive(Clone, Debug)]
49pub enum ChainSourceSpec {
50 Bitcoind {
51 url: String,
53 auth: rpc::Auth,
55 },
56 Esplora {
57 url: String,
59 },
60}
61
62impl ChainSourceSpec {
63 pub(crate) fn url(&self) -> &String {
64 match self {
65 ChainSourceSpec::Bitcoind { url, .. } => url,
66 ChainSourceSpec::Esplora { url } => url,
67 }
68 }
69}
70
71pub enum ChainSourceClient {
72 #[cfg(feature = "bitcoind-rpc")]
78 Bitcoind {
79 rpc: BitcoindClient,
80 sync: BitcoinRpcClient,
81 },
82 Esplora(esplora_client::AsyncClient),
83}
84
85impl ChainSourceClient {
86 async fn check_network(&self, expected: Network) -> anyhow::Result<()> {
87 match self {
88 #[cfg(feature = "bitcoind-rpc")]
89 ChainSourceClient::Bitcoind { rpc, .. } => {
90 let network = rpc.network().await?;
91 if expected != network {
92 bail!("Network mismatch: expected {:?}, got {:?}", expected, network);
93 }
94 },
95 ChainSourceClient::Esplora(client) => {
96 let res = client.client().get(format!("{}/block-height/0", client.url()))
97 .send().await?.text().await?;
98 let genesis_hash = BlockHash::from_str(&res)
99 .context("bad response from server (not a blockhash). Esplora client possibly misconfigured")?;
100 if genesis_hash != genesis_block(expected).block_hash() {
101 bail!("Network mismatch: expected {:?}, got {:?}", expected, genesis_hash);
102 }
103 },
104 };
105
106 Ok(())
107 }
108}
109
110pub struct ChainSource {
144 inner: ChainSourceClient,
145 network: Network,
146 fee_rates: RwLock<FeeRates>,
147}
148
149impl ChainSource {
150 pub async fn require_version(&self) -> anyhow::Result<()> {
155 #[cfg(feature = "bitcoind-rpc")]
156 if let ChainSourceClient::Bitcoind { rpc, .. } = self.inner() {
157 #[derive(Debug, serde::Deserialize)]
158 struct NetworkInfo { version: usize }
159 let info: NetworkInfo = rpc.call_raw("getnetworkinfo", &[]).await?;
160 if info.version < MIN_BITCOIND_VERSION {
161 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.");
162 }
163 }
164
165 Ok(())
166 }
167
168 pub(crate) fn inner(&self) -> &ChainSourceClient {
169 &self.inner
170 }
171
172 pub async fn fee_rates(&self) -> FeeRates {
174 self.fee_rates.read().await.clone()
175 }
176
177 pub fn network(&self) -> Network {
179 self.network
180 }
181
182 pub async fn new(
215 spec: ChainSourceSpec,
216 network: Network,
217 fallback_fee: Option<FeeRate>,
218 #[cfg(feature = "socks5-proxy")] proxy: Option<&str>,
219 ) -> anyhow::Result<Self> {
220 let inner = match spec {
221 #[cfg(feature = "bitcoind-rpc")]
222 ChainSourceSpec::Bitcoind { url, auth } => {
223 let sync = BitcoinRpcClient::new(&url, auth.clone())
233 .context("failed to create sync bitcoind rpc client")?;
234 let async_auth = match auth {
235 rpc::Auth::None => bail!(
236 "bitcoind RPC auth is required (cookie file or user/pass)",
237 ),
238 rpc::Auth::UserPass(u, p) => bitcoind_async_client::Auth::UserPass(u, p),
239 rpc::Auth::CookieFile(p) => bitcoind_async_client::Auth::CookieFile(p),
240 };
241 let rpc = BitcoindClient::new(url, async_auth, None, None, None)
242 .context("failed to create async bitcoind rpc client")?;
243 ChainSourceClient::Bitcoind { rpc, sync }
244 },
245 #[cfg(not(feature = "bitcoind-rpc"))]
246 ChainSourceSpec::Bitcoind { .. } => bail!(
247 "bitcoind RPC backend is not available: this build was compiled without \
248 the `bitcoind-rpc` feature (notably the wasm-web build)",
249 ),
250 ChainSourceSpec::Esplora { url } => ChainSourceClient::Esplora({
251 let url = url.strip_suffix("/").unwrap_or(&url);
253 let mut builder = esplora_client::Builder::new(url);
254 #[cfg(feature = "socks5-proxy")]
255 if let Some(proxy) = proxy {
256 builder = builder.proxy(proxy);
257 }
258 builder.build_async()
259 .with_context(|| format!("failed to create esplora client for url {}", url))?
260 }),
261 };
262
263 inner.check_network(network).await?;
264
265 let fee = fallback_fee.unwrap_or(FeeRate::BROADCAST_MIN);
266 let fee_rates = RwLock::new(FeeRates { fast: fee, regular: fee, slow: fee });
267
268 Ok(Self { inner, network, fee_rates })
269 }
270
271 async fn fetch_fee_rates(&self) -> anyhow::Result<FeeRates> {
272 match self.inner() {
273 #[cfg(feature = "bitcoind-rpc")]
274 ChainSourceClient::Bitcoind { rpc, .. } => {
275 let get_fee_rate = async |target: u16| -> anyhow::Result<FeeRate> {
276 let fee: rpc::json::EstimateSmartFeeResult = rpc.call_raw(
277 "estimatesmartfee",
278 &[
279 target.into(),
280 serde_json::to_value(rpc::json::EstimateMode::Economical)
281 .expect("serializable"),
282 ],
283 ).await?;
284 if let Some(fee_rate) = fee.fee_rate {
285 Ok(FeeRate::from_amount_per_kvb_ceil(fee_rate))
286 } else {
287 Err(anyhow!("No rate returned from estimate_smart_fee for a {} confirmation target", target))
288 }
289 };
290 Ok(FeeRates {
291 fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST).await?,
292 regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR).await.expect("should exist"),
293 slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW).await.expect("should exist"),
294 })
295 },
296 ChainSourceClient::Esplora(client) => {
297 let estimates = client.get_fee_estimates().await?;
299 let get_fee_rate = |target| {
300 let fee = estimates.get(&target).with_context(||
301 format!("No rate returned from get_fee_estimates for a {} confirmation target", target)
302 )?;
303 FeeRate::from_sat_per_vb_decimal_checked_ceil(*fee).with_context(||
304 format!("Invalid rate returned from get_fee_estimates {} for a {} confirmation target", fee, target)
305 )
306 };
307 Ok(FeeRates {
308 fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST)?,
309 regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR)?,
310 slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW)?,
311 })
312 }
313 }
314 }
315
316 pub async fn tip(&self) -> anyhow::Result<BlockHeight> {
317 match self.inner() {
318 #[cfg(feature = "bitcoind-rpc")]
319 ChainSourceClient::Bitcoind { rpc, .. } => {
320 let count = rpc.get_block_count().await?;
321 Ok(count as BlockHeight)
322 },
323 ChainSourceClient::Esplora(client) => {
324 Ok(client.get_height().await?)
325 },
326 }
327 }
328
329 pub async fn tip_ref(&self) -> anyhow::Result<BlockRef> {
330 self.block_ref(self.tip().await?).await
331 }
332
333 pub async fn block_ref(&self, height: BlockHeight) -> anyhow::Result<BlockRef> {
334 match self.inner() {
335 #[cfg(feature = "bitcoind-rpc")]
336 ChainSourceClient::Bitcoind { rpc, .. } => {
337 let hash = rpc.get_block_hash(height as u64).await?;
338 Ok(BlockRef { height, hash })
339 },
340 ChainSourceClient::Esplora(client) => {
341 let hash = client.get_block_hash(height).await?;
342 Ok(BlockRef { height, hash })
343 },
344 }
345 }
346
347 pub async fn block(&self, hash: BlockHash) -> anyhow::Result<Option<Block>> {
348 match self.inner() {
349 #[cfg(feature = "bitcoind-rpc")]
350 ChainSourceClient::Bitcoind { rpc, .. } => {
351 match rpc.get_block(&hash).await {
352 Ok(block) => Ok(Some(block)),
353 Err(e) if is_not_found(&e) => Ok(None),
354 Err(e) => Err(e.into()),
355 }
356 },
357 ChainSourceClient::Esplora(client) => {
358 Ok(client.get_block_by_hash(&hash).await?)
359 },
360 }
361 }
362
363 pub async fn mempool_ancestor_info(&self, txid: Txid) -> anyhow::Result<MempoolAncestorInfo> {
366 let mut result = MempoolAncestorInfo::new(txid);
367
368 match self.inner() {
371 #[cfg(feature = "bitcoind-rpc")]
372 ChainSourceClient::Bitcoind { rpc, .. } => {
373 let entry: rpc::json::GetMempoolEntryResult = rpc.call_raw(
374 "getmempoolentry", &[serde_json::to_value(txid).expect("serializable")],
375 ).await?;
376 let err = || anyhow!("missing weight parameter from getmempoolentry");
377
378 result.total_fee = entry.fees.ancestor;
379 result.total_weight = Weight::from_wu(entry.weight.ok_or_else(err)?) +
380 Weight::from_vb(entry.ancestor_size).ok_or_else(err)?;
381 },
382 ChainSourceClient::Esplora(client) => {
383 let status = self.tx_status(txid).await?;
386 if !matches!(status, TxStatus::Mempool) {
387 return Err(anyhow!("{} is not in the mempool, status is {:?}", txid, status));
388 }
389
390 let mut info_map: HashMap<Txid, esplora_client::Tx> = HashMap::new();
391 let mut set = HashSet::from([txid]);
392 while !set.is_empty() {
393 let requests = set.iter().filter_map(|txid| if info_map.contains_key(txid) {
395 None
396 } else {
397 Some((txid, client.get_tx_info(&txid)))
398 }).collect::<Vec<_>>();
399
400 let mut next_set = HashSet::new();
402
403 for (txid, request) in requests {
405 let info = request.await?
406 .ok_or_else(|| anyhow!("unable to retrieve tx info for {}", txid))?;
407 if !info.status.confirmed {
408 for vin in info.vin.iter() {
409 next_set.insert(vin.txid);
410 }
411 }
412 info_map.insert(*txid, info);
413 }
414 set = next_set;
415 }
416 for info in info_map.into_values().filter(|info| !info.status.confirmed) {
418 result.total_fee += info.fee();
419 result.total_weight += info.weight();
420 }
421 },
422 }
423 Ok(result)
425 }
426
427 pub async fn txs_spending_inputs<T: IntoIterator<Item = OutPoint>>(
430 &self,
431 outpoints: T,
432 #[cfg_attr(not(feature = "bitcoind-rpc"), allow(unused_variables))]
433 block_scan_start: BlockHeight,
434 ) -> anyhow::Result<TxsSpendingInputsResult> {
435 let mut res = TxsSpendingInputsResult::new();
436 match self.inner() {
437 #[cfg(feature = "bitcoind-rpc")]
438 ChainSourceClient::Bitcoind { sync, .. } => {
439 let start = block_scan_start.saturating_sub(1);
441 let block_ref = self.block_ref(start).await?;
442 let cp = CheckPoint::new(BlockId {
443 height: block_ref.height,
444 hash: block_ref.hash,
445 });
446
447 debug!("Scanning blocks for spent outpoints with bitcoind, starting at block height {}...", block_scan_start);
448 let outpoint_set = outpoints.into_iter().collect::<HashSet<_>>();
449
450 let sync_client = sync.clone();
453 let cp_for_blocking = cp.clone();
454 res = tokio::task::spawn_blocking(move || -> anyhow::Result<TxsSpendingInputsResult> {
455 let mut res = res;
456 let mut emitter = bdk_bitcoind_rpc::Emitter::new(
457 &sync_client,
458 cp_for_blocking.clone(),
459 cp_for_blocking.height(),
460 bdk_bitcoind_rpc::NO_EXPECTED_MEMPOOL_TXS,
461 );
462 while let Some(em) = emitter.next_block()? {
463 if em.block_height() % 1000 == 0 {
464 info!("Scanned for spent outpoints until block height {}", em.block_height());
465 }
466 for tx in &em.block.txdata {
467 for txin in tx.input.iter() {
468 if outpoint_set.contains(&txin.previous_output) {
469 res.add(
470 txin.previous_output.clone(),
471 tx.compute_txid(),
472 TxStatus::Confirmed(BlockRef {
473 height: em.block_height(),
474 hash: em.block.block_hash().clone(),
475 }),
476 );
477 if res.map.len() == outpoint_set.len() {
478 return Ok(res);
479 }
480 }
481 }
482 }
483 }
484
485 debug!("Finished scanning blocks for spent outpoints, now checking the mempool...");
486 let mempool = emitter.mempool()?;
487 for (tx, _last_seen) in &mempool.update {
488 for txin in tx.input.iter() {
489 if outpoint_set.contains(&txin.previous_output) {
490 res.add(
491 txin.previous_output.clone(),
492 tx.compute_txid(),
493 TxStatus::Mempool,
494 );
495 if res.map.len() == outpoint_set.len() {
496 return Ok(res);
497 }
498 }
499 }
500 }
501 debug!("Finished checking the mempool for spent outpoints");
502 Ok(res)
503 }).await.context("Emitter scan task panicked")??;
504 },
505 ChainSourceClient::Esplora(client) => {
506 for outpoint in outpoints {
507 let output_status = client.get_output_status(&outpoint.txid, outpoint.vout.into()).await?;
508
509 if let Some(output_status) = output_status {
510 if output_status.spent {
511 let tx_status = {
512 let status = output_status.status.expect("Status should be valid if an outpoint is spent");
513 if status.confirmed {
514 TxStatus::Confirmed(BlockRef {
515 height: status.block_height.expect("Confirmed transaction missing block_height"),
516 hash: status.block_hash.expect("Confirmed transaction missing block_hash"),
517 })
518 } else {
519 TxStatus::Mempool
520 }
521 };
522 let txid = output_status.txid.expect("Txid should be valid if an outpoint is spent");
523 res.add(outpoint, txid, tx_status);
524 }
525 }
526 }
527 },
528 }
529
530 Ok(res)
531 }
532
533 pub async fn broadcast_tx(&self, tx: &Transaction) -> anyhow::Result<()> {
534 match self.inner() {
535 #[cfg(feature = "bitcoind-rpc")]
536 ChainSourceClient::Bitcoind { rpc, .. } => {
537 match rpc.send_raw_transaction(tx).await {
538 Ok(_) => Ok(()),
539 Err(e) if is_in_utxo_set(&e) => Ok(()),
540 Err(e) => Err(e.into()),
541 }
542 },
543 ChainSourceClient::Esplora(client) => {
544 client.broadcast(tx).await?;
545 Ok(())
546 },
547 }
548 }
549
550 pub async fn broadcast_package(&self, txs: &[impl Borrow<Transaction>]) -> anyhow::Result<()> {
551 match self.inner() {
552 #[cfg(feature = "bitcoind-rpc")]
553 ChainSourceClient::Bitcoind { rpc, .. } => {
554 let hexes: Vec<String> = txs.iter()
555 .map(|t| bitcoin::consensus::encode::serialize_hex(t.borrow()))
556 .collect();
557 let res: rpc::SubmitPackageResult =
558 rpc.call_raw("submitpackage", &[hexes.into()]).await?;
559 if res.package_msg != "success" {
560 let errors = res.tx_results.values()
561 .map(|t| format!("tx {}: {}",
562 t.txid, t.error.as_ref().map(|s| s.as_str()).unwrap_or("(no error)"),
563 ))
564 .collect::<Vec<_>>();
565 bail!("msg: '{}', errors: {:?}", res.package_msg, errors);
566 }
567 Ok(())
568 },
569 ChainSourceClient::Esplora(client) => {
570 let txs = txs.iter().map(|t| t.borrow().clone()).collect::<Vec<_>>();
571 let res = client.submit_package(&txs, None, None).await?;
572 if res.package_msg != "success" {
573 let errors = res.tx_results.values()
574 .map(|t| format!("tx {}: {}",
575 t.txid, t.error.as_ref().map(|s| s.as_str()).unwrap_or("(no error)"),
576 ))
577 .collect::<Vec<_>>();
578 bail!("msg: '{}', errors: {:?}", res.package_msg, errors);
579 }
580
581 Ok(())
582 },
583 }
584 }
585
586 pub async fn get_tx(&self, txid: &Txid) -> anyhow::Result<Option<Transaction>> {
587 match self.inner() {
588 #[cfg(feature = "bitcoind-rpc")]
589 ChainSourceClient::Bitcoind { rpc, .. } => {
590 match rpc.get_raw_transaction_verbosity_zero(txid).await {
591 Ok(tx) => Ok(Some(tx.0)),
592 Err(e) if is_not_found(&e) => Ok(None),
593 Err(e) => Err(e.into()),
594 }
595 },
596 ChainSourceClient::Esplora(client) => {
597 Ok(client.get_tx(txid).await?)
598 },
599 }
600 }
601
602 pub async fn tx_confirmed(&self, txid: Txid) -> anyhow::Result<Option<BlockHeight>> {
604 Ok(self.tx_status(txid).await?.confirmed_height())
605 }
606
607 pub async fn tx_status(&self, txid: Txid) -> anyhow::Result<TxStatus> {
609 match self.inner() {
610 #[cfg(feature = "bitcoind-rpc")]
611 ChainSourceClient::Bitcoind { rpc, .. } => Ok(bitcoind_tx_status(rpc, txid).await?),
612 ChainSourceClient::Esplora(esplora) => {
613 match esplora.get_tx_info(&txid).await? {
614 Some(info) => match (info.status.block_height, info.status.block_hash) {
615 (Some(block_height), Some(block_hash)) => Ok(TxStatus::Confirmed(BlockRef {
616 height: block_height,
617 hash: block_hash,
618 } )),
619 _ => Ok(TxStatus::Mempool),
620 },
621 None => Ok(TxStatus::NotFound),
622 }
623 },
624 }
625 }
626
627 #[allow(unused)]
628 pub async fn txout_value(&self, outpoint: &OutPoint) -> anyhow::Result<Amount> {
629 let tx = match self.inner() {
630 #[cfg(feature = "bitcoind-rpc")]
631 ChainSourceClient::Bitcoind { rpc, .. } => {
632 rpc.get_raw_transaction_verbosity_zero(&outpoint.txid).await
633 .with_context(|| format!("tx {} unknown", outpoint.txid))?
634 .0
635 },
636 ChainSourceClient::Esplora(client) => {
637 client.get_tx(&outpoint.txid).await?
638 .with_context(|| format!("tx {} unknown", outpoint.txid))?
639 },
640 };
641 Ok(tx.output.get(outpoint.vout as usize).context("outpoint vout out of range")?.value)
642 }
643
644 pub async fn update_fee_rates(&self, fallback_fee: Option<FeeRate>) -> anyhow::Result<()> {
647 let fee_rates = match (self.fetch_fee_rates().await, fallback_fee) {
648 (Ok(fee_rates), _) => Ok(fee_rates),
649 (Err(e), None) => Err(e),
650 (Err(e), Some(fallback)) => {
651 warn!("Error getting fee rates, falling back to {} sat/kvB: {}",
652 fallback.to_btc_per_kvb(), e,
653 );
654 Ok(FeeRates { fast: fallback, regular: fallback, slow: fallback })
655 }
656 }?;
657
658 *self.fee_rates.write().await = fee_rates;
659 Ok(())
660 }
661}
662
663#[cfg(feature = "bitcoind-rpc")]
669fn is_not_found(e: &BitcoindClientError) -> bool {
670 matches!(e, BitcoindClientError::Server(c, _) if *c == RPC_INVALID_ADDRESS_OR_KEY)
671}
672
673#[cfg(feature = "bitcoind-rpc")]
675fn is_in_utxo_set(e: &BitcoindClientError) -> bool {
676 matches!(e, BitcoindClientError::Server(c, _) if *c == RPC_VERIFY_ALREADY_IN_UTXO_SET)
677}
678
679#[cfg(feature = "bitcoind-rpc")]
682async fn bitcoind_tx_status(
683 rpc: &BitcoindClient, txid: Txid,
684) -> Result<TxStatus, BitcoindClientError> {
685 let res: Result<rpc::GetRawTransactionResult, _> = rpc.call_raw(
686 "getrawtransaction",
687 &[serde_json::to_value(txid).expect("serializable"), true.into()],
688 ).await;
689 let info = match res {
690 Ok(info) => info,
691 Err(e) if is_not_found(&e) => return Ok(TxStatus::NotFound),
692 Err(e) => return Err(e),
693 };
694 let Some(hash) = info.blockhash else {
695 return Ok(TxStatus::Mempool);
696 };
697 let header: rpc::json::GetBlockHeaderResult = rpc.call_raw(
698 "getblockheader",
699 &[serde_json::to_value(hash).expect("serializable"), true.into()],
700 ).await?;
701 if header.confirmations > 0 {
702 Ok(TxStatus::Confirmed(BlockRef {
703 height: header.height as BlockHeight,
704 hash: header.hash,
705 }))
706 } else {
707 Ok(TxStatus::Mempool)
708 }
709}
710
711#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
713pub struct FeeRates {
714 pub fast: FeeRate,
716 pub regular: FeeRate,
718 pub slow: FeeRate,
720}
721
722#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
724pub struct MempoolAncestorInfo {
725 pub txid: Txid,
727 pub total_fee: Amount,
730 pub total_weight: Weight,
732}
733
734impl MempoolAncestorInfo {
735 pub fn new(txid: Txid) -> Self {
736 Self {
737 txid,
738 total_fee: Amount::ZERO,
739 total_weight: Weight::ZERO,
740 }
741 }
742
743 pub fn effective_fee_rate(&self) -> Option<FeeRate> {
744 FeeRate::from_amount_and_weight_ceil(self.total_fee, self.total_weight)
745 }
746}
747
748#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
749pub struct TxsSpendingInputsResult {
750 pub map: HashMap<OutPoint, (Txid, TxStatus)>,
751}
752
753impl TxsSpendingInputsResult {
754 pub fn new() -> Self {
755 Self { map: HashMap::new() }
756 }
757
758 pub fn add(&mut self, outpoint: OutPoint, txid: Txid, status: TxStatus) {
759 self.map.insert(outpoint, (txid, status));
760 }
761
762 pub fn get(&self, outpoint: &OutPoint) -> Option<&(Txid, TxStatus)> {
763 self.map.get(outpoint)
764 }
765
766 pub fn confirmed_txids(&self) -> impl Iterator<Item = (Txid, BlockRef)> + '_ {
767 self.map
768 .iter()
769 .filter_map(|(_, (txid, status))| {
770 match status {
771 TxStatus::Confirmed(block) => Some((*txid, *block)),
772 _ => None,
773 }
774 })
775 }
776
777 pub fn mempool_txids(&self) -> impl Iterator<Item = Txid> + '_ {
778 self.map
779 .iter()
780 .filter(|(_, (_, status))| matches!(status, TxStatus::Mempool))
781 .map(|(_, (txid, _))| *txid)
782 }
783}