bark/onchain/
chain.rs

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, Wtxid,
13};
14use log::{debug, info, warn};
15use tokio::sync::RwLock;
16
17use bitcoin_ext::{BlockHeight, BlockRef, FeeRateExt, TxStatus};
18use bitcoin_ext::rpc::{self, BitcoinRpcExt, BitcoinRpcErrorExt, RpcApi};
19use bitcoin_ext::esplora::EsploraClientExt;
20
21const FEE_RATE_TARGET_CONF_FAST: u16 = 1;
22const FEE_RATE_TARGET_CONF_REGULAR: u16 = 3;
23const FEE_RATE_TARGET_CONF_SLOW: u16 = 6;
24
25const TX_ALREADY_IN_CHAIN_ERROR: i32 = -27;
26const MIN_BITCOIND_VERSION: usize = 290000;
27
28/// Configuration for the onchain data source.
29///
30/// [ChainSource] selects which backend to use for blockchain data and transaction broadcasting:
31/// - Bitcoind: uses a Bitcoin Core node via JSON-RPC
32/// - Esplora: uses the HTTP API endpoint of [esplora-electrs](https://github.com/Blockstream/electrs)
33///
34/// Typical usage is to construct a ChainSource from configuration and pass it to
35/// [ChainSource::new] along with the expected [Network].
36///
37/// Notes:
38/// - For [ChainSourceSpec::Bitcoind], authentication must be provided (cookie file or user/pass).
39#[derive(Clone, Debug)]
40pub enum ChainSourceSpec {
41	Bitcoind {
42		/// RPC URL of the Bitcoin Core node (e.g. <http://127.0.0.1:8332>).
43		url: String,
44		/// Authentication method for JSON-RPC (cookie file or user/pass).
45		auth: rpc::Auth,
46	},
47	Esplora {
48		/// Base URL of the esplora-electrs instance (e.g. <https://esplora.signet.2nd.dev>).
49		url: String,
50	},
51}
52
53pub enum ChainSourceClient {
54	Bitcoind(rpc::Client),
55	Esplora(esplora_client::AsyncClient),
56}
57
58impl ChainSourceClient {
59	async fn check_network(&self, expected: Network) -> anyhow::Result<()> {
60		match self {
61			ChainSourceClient::Bitcoind(bitcoind) => {
62				let network = bitcoind.get_blockchain_info()?;
63				if expected != network.chain {
64					bail!("Network mismatch: expected {:?}, got {:?}", expected, network.chain);
65				}
66			},
67			ChainSourceClient::Esplora(client) => {
68				let res = client.client().get(format!("{}/block-height/0", client.url()))
69					.send().await?.text().await?;
70				let genesis_hash = BlockHash::from_str(&res)
71					.context("bad response from server (not a blockhash). Esplora client possibly misconfigured")?;
72				if genesis_hash != genesis_block(expected).block_hash() {
73					bail!("Network mismatch: expected {:?}, got {:?}", expected, genesis_hash);
74				}
75			},
76		};
77
78		Ok(())
79	}
80}
81
82/// Client for interacting with the configured on-chain backend.
83///
84/// [ChainSource] abstracts over multiple backends using [ChainSourceSpec] to provide:
85/// - Chain queries (tip, block headers/blocks, transaction status and fetching)
86/// - Mempool-related utilities (ancestor fee/weight, spending lookups)
87/// - Broadcasting single transactions or packages (RBF/CPFP workflows)
88/// - Fee estimation and caching with optional fallback values
89///
90/// Behavior notes:
91/// - [ChainSource::update_fee_rates] refreshes internal fee estimates; if backend estimates
92///   fail and a fallback fee is provided, it will be used for all tiers.
93/// - [ChainSource::fee_rates] returns the last cached [FeeRates].
94///
95/// Examples:
96///
97/// ```rust
98/// # async fn func() {
99/// use bark::onchain::{ChainSource, ChainSourceSpec};
100/// use bdk_bitcoind_rpc::bitcoincore_rpc::Auth;
101/// use bitcoin::{FeeRate, Network};
102///
103/// let spec = ChainSourceSpec::Bitcoind {
104///     url: "http://localhost:8332".into(),
105///     auth: Auth::UserPass("user".into(), "password".into()),
106/// };
107/// let network = Network::Bitcoin;
108/// let fallback_fee = FeeRate::from_sat_per_vb(5);
109///
110/// let instance = ChainSource::new(spec, network, fallback_fee).await.unwrap();
111/// # }
112/// ```
113pub struct ChainSource {
114	inner: ChainSourceClient,
115	network: Network,
116	fee_rates: RwLock<FeeRates>,
117}
118
119impl ChainSource {
120	/// Checks that the version of the chain source is compatible with Bark.
121	///
122	/// For bitcoind, it checks if the version is at least 29.0
123	/// This is the first version for which 0 fee-anchors are considered standard
124	pub fn require_version(&self) -> anyhow::Result<()> {
125		if let ChainSourceClient::Bitcoind(bitcoind) = self.inner() {
126			if bitcoind.version()? < MIN_BITCOIND_VERSION {
127				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.");
128			}
129		}
130
131		Ok(())
132	}
133
134	pub(crate) fn inner(&self) -> &ChainSourceClient {
135		&self.inner
136	}
137
138	/// Gets a cached copy of the calculated network [FeeRates]
139	pub async fn fee_rates(&self) -> FeeRates {
140		self.fee_rates.read().await.clone()
141	}
142
143	/// Gets the network that the [ChainSource] was validated against.
144	pub fn network(&self) -> Network {
145		self.network
146	}
147
148	/// Creates a new instance of the object with the specified chain source, network, and optional
149	/// fallback fee rate.
150	///
151	/// This function initializes the internal chain source client based on the provided `chain_source`:
152	/// - If `chain_source` is of type [ChainSourceSpec::Bitcoind], it creates a Bitcoin Core RPC client
153	///   using the provided URL and authentication parameters.
154	/// - If `chain_source` is of type [ChainSourceSpec::Esplora], it creates an Esplora client with the
155	///   given URL.
156	///
157	/// Both clients are initialized asynchronously, and any errors encountered during their
158	/// creation will be returned as part of the [anyhow::Result].
159	///
160	/// Additionally, the function performs a network consistency check to ensure the specified
161	/// network (e.g., `mainnet` or `signet`) matches the network configuration of the initialized
162	/// chain source client.
163	///
164	/// The `fallback_fee` parameter is optional. If provided, it is used as the default fee rate
165	/// for transactions. If not specified, the `FeeRate::BROADCAST_MIN` is used as the default fee
166	/// rate.
167	///
168	/// # Arguments
169	///
170	/// * `chain_source` - Specifies the backend to use for blockchain data.
171	/// * `network` - The Bitcoin network to operate on (e.g., `mainnet`, `testnet`, `regtest`).
172	/// * `fallback_fee` - An optional fallback fee rate to use for transaction fee estimation. If
173	///   not provided, a default fee rate of [FeeRate::BROADCAST_MIN] will be used.
174	///
175	/// # Returns
176	///
177	/// * `Ok(Self)` - If the object is successfully created with all necessary configurations.
178	/// * `Err(anyhow::Error)` - If there is an error in initializing the chain source client or
179	///   verifying the network.
180	pub async fn new(spec: ChainSourceSpec, network: Network, fallback_fee: Option<FeeRate>) -> anyhow::Result<Self> {
181		let inner = match spec {
182			ChainSourceSpec::Bitcoind { url, auth } => ChainSourceClient::Bitcoind(
183				rpc::Client::new(&url, auth)
184					.context("failed to create bitcoind rpc client")?
185			),
186			ChainSourceSpec::Esplora { url } => ChainSourceClient::Esplora({
187				// the esplora client doesn't deal well with trailing slash in url
188				let url = url.strip_suffix("/").unwrap_or(&url);
189				esplora_client::Builder::new(url).build_async()
190					.with_context(|| format!("failed to create esplora client for url {}", url))?
191			}),
192		};
193
194		inner.check_network(network).await?;
195
196		let fee = fallback_fee.unwrap_or(FeeRate::BROADCAST_MIN);
197		let fee_rates = RwLock::new(FeeRates { fast: fee, regular: fee, slow: fee });
198
199		Ok(Self { inner, network, fee_rates })
200	}
201
202	async fn fetch_fee_rates(&self) -> anyhow::Result<FeeRates> {
203		match self.inner() {
204			ChainSourceClient::Bitcoind(bitcoind) => {
205				let get_fee_rate = |target| {
206					let fee = bitcoind.estimate_smart_fee(
207						target, Some(rpc::json::EstimateMode::Economical),
208					)?;
209					if let Some(fee_rate) = fee.fee_rate {
210						Ok(FeeRate::from_amount_per_kvb_ceil(fee_rate))
211					} else {
212						Err(anyhow!("No rate returned from estimate_smart_fee for a {} confirmation target", target))
213					}
214				};
215				Ok(FeeRates {
216					fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST)?,
217					regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR).expect("should exist"),
218					slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW).expect("should exist"),
219				})
220			},
221			ChainSourceClient::Esplora(client) => {
222				// The API should return rates for targets 1-25, 144 and 1008
223				let estimates = client.get_fee_estimates().await?;
224				let get_fee_rate = |target| {
225					let fee = estimates.get(&target).with_context(||
226						format!("No rate returned from get_fee_estimates for a {} confirmation target", target)
227					)?;
228					FeeRate::from_sat_per_vb_decimal_checked_ceil(*fee).with_context(||
229						format!("Invalid rate returned from get_fee_estimates {} for a {} confirmation target", fee, target)
230					)
231				};
232				Ok(FeeRates {
233					fast: get_fee_rate(FEE_RATE_TARGET_CONF_FAST)?,
234					regular: get_fee_rate(FEE_RATE_TARGET_CONF_REGULAR)?,
235					slow: get_fee_rate(FEE_RATE_TARGET_CONF_SLOW)?,
236				})
237			}
238		}
239	}
240
241	pub async fn tip(&self) -> anyhow::Result<BlockHeight> {
242		match self.inner() {
243			ChainSourceClient::Bitcoind(bitcoind) => {
244				Ok(bitcoind.get_block_count()? as BlockHeight)
245			},
246			ChainSourceClient::Esplora(client) => {
247				Ok(client.get_height().await?)
248			},
249		}
250	}
251
252	pub async fn tip_ref(&self) -> anyhow::Result<BlockRef> {
253		self.block_ref(self.tip().await?).await
254	}
255
256	pub async fn block_ref(&self, height: BlockHeight) -> anyhow::Result<BlockRef> {
257		match self.inner() {
258			ChainSourceClient::Bitcoind(bitcoind) => {
259				let hash = bitcoind.get_block_hash(height as u64)?;
260				Ok(BlockRef { height, hash })
261			},
262			ChainSourceClient::Esplora(client) => {
263				let hash = client.get_block_hash(height).await?;
264				Ok(BlockRef { height, hash })
265			},
266		}
267	}
268
269	pub async fn block(&self, hash: BlockHash) -> anyhow::Result<Option<Block>> {
270		match self.inner() {
271			ChainSourceClient::Bitcoind(bitcoind) => {
272				match bitcoind.get_block(&hash) {
273					Ok(b) => Ok(Some(b)),
274					Err(e) if e.is_not_found() => Ok(None),
275					Err(e) => Err(e.into()),
276				}
277			},
278			ChainSourceClient::Esplora(client) => {
279				Ok(client.get_block_by_hash(&hash).await?)
280			},
281		}
282	}
283
284	/// Retrieves basic CPFP ancestry information of the given transaction. Confirmed transactions
285	/// are ignored as they are not relevant to CPFP.
286	pub async fn mempool_ancestor_info(&self, txid: Txid) -> anyhow::Result<MempoolAncestorInfo> {
287		let mut result = MempoolAncestorInfo::new(txid);
288
289		// TODO: Determine if any line of descendant transactions increase the effective fee rate
290		//		 of the target txid.
291		match self.inner() {
292			ChainSourceClient::Bitcoind(bitcoind) => {
293				let entry = bitcoind.get_mempool_entry(&txid)?;
294				let err = || anyhow!("missing weight parameter from getmempoolentry");
295
296				result.total_fee = entry.fees.ancestor;
297				result.total_weight = Weight::from_wu(entry.weight.ok_or_else(err)?) +
298					Weight::from_vb(entry.ancestor_size).ok_or_else(err)?;
299			},
300			ChainSourceClient::Esplora(client) => {
301				// We should first verify the transaction is in the mempool to maintain the same
302				// behavior as Bitcoin Core
303				let status = self.tx_status(txid).await?;
304				if !matches!(status, TxStatus::Mempool) {
305					return Err(anyhow!("{} is not in the mempool, status is {:?}", txid, status));
306				}
307
308				let mut info_map: HashMap<Txid, esplora_client::Tx> = HashMap::new();
309				let mut set = HashSet::from([txid]);
310				while !set.is_empty() {
311					// Start requests asynchronously
312					let requests = set.iter().filter_map(|txid| if info_map.contains_key(txid) {
313						None
314					} else {
315						Some((txid, client.get_tx_info(&txid)))
316					}).collect::<Vec<_>>();
317
318					// Collect txids to be added to the set
319					let mut next_set = HashSet::new();
320
321					// Process each request, ignoring parents of confirmed transactions
322					for (txid, request) in requests {
323						let info = request.await?
324							.ok_or_else(|| anyhow!("unable to retrieve tx info for {}", txid))?;
325						if !info.status.confirmed {
326							for vin in info.vin.iter() {
327								next_set.insert(vin.txid);
328							}
329						}
330						info_map.insert(*txid, info);
331					}
332					set = next_set;
333				}
334				// Calculate the total weight and fee of the unconfirmed ancestry
335				for info in info_map.into_values().filter(|info| !info.status.confirmed) {
336					result.total_fee += info.fee();
337					result.total_weight += info.weight();
338				}
339			},
340		}
341		// Now calculate the effective fee rate of the package
342		Ok(result)
343	}
344
345	/// For each provided outpoint, fetches the ID of any confirmed or unconfirmed in which the
346	/// outpoint is spent.
347	pub async fn txs_spending_inputs<T: IntoIterator<Item = OutPoint>>(
348		&self,
349		outpoints: T,
350		block_scan_start: BlockHeight,
351	) -> anyhow::Result<TxsSpendingInputsResult> {
352		let mut res = TxsSpendingInputsResult::new();
353		match self.inner() {
354			ChainSourceClient::Bitcoind(bitcoind) => {
355				// We must offset the height to account for the fact we iterate using next_block()
356				let start = block_scan_start.saturating_sub(1);
357				let block_ref = self.block_ref(start).await?;
358				let cp = CheckPoint::new(BlockId {
359					height: block_ref.height,
360					hash: block_ref.hash,
361				});
362
363				let mut emitter = bdk_bitcoind_rpc::Emitter::new(
364					bitcoind, cp.clone(), cp.height(), bdk_bitcoind_rpc::NO_EXPECTED_MEMPOOL_TXS,
365				);
366
367				debug!("Scanning blocks for spent outpoints with bitcoind, starting at block height {}...", block_scan_start);
368				let outpoint_set = outpoints.into_iter().collect::<HashSet<_>>();
369				while let Some(em) = emitter.next_block()? {
370					// Provide updates as the scan can take a long time
371					if em.block_height() % 1000 == 0 {
372						info!("Scanned for spent outpoints until block height {}", em.block_height());
373					}
374					for tx in &em.block.txdata {
375						for txin in tx.input.iter() {
376							if outpoint_set.contains(&txin.previous_output) {
377								res.add(
378									txin.previous_output.clone(),
379									tx.compute_txid(),
380									TxStatus::Confirmed(BlockRef {
381										height: em.block_height(), hash: em.block.block_hash().clone()
382									})
383								);
384								// We can stop early if we've found a spending tx for each outpoint
385								if res.map.len() == outpoint_set.len() {
386									return Ok(res);
387								}
388							}
389						}
390					}
391				}
392
393				debug!("Finished scanning blocks for spent outpoints, now checking the mempool...");
394				let mempool = emitter.mempool()?;
395				for (tx, _last_seen) in &mempool.update {
396					for txin in tx.input.iter() {
397						if outpoint_set.contains(&txin.previous_output) {
398							res.add(
399								txin.previous_output.clone(),
400								tx.compute_txid(),
401								TxStatus::Mempool,
402							);
403
404							// We can stop early if we've found a spending tx for each outpoint
405							if res.map.len() == outpoint_set.len() {
406								return Ok(res);
407							}
408						}
409					}
410				}
411				debug!("Finished checking the mempool for spent outpoints");
412			},
413			ChainSourceClient::Esplora(client) => {
414				for outpoint in outpoints {
415					let output_status = client.get_output_status(&outpoint.txid, outpoint.vout.into()).await?;
416
417					if let Some(output_status) = output_status {
418						if output_status.spent {
419							let tx_status = {
420								let status = output_status.status.expect("Status should be valid if an outpoint is spent");
421								if status.confirmed {
422									TxStatus::Confirmed(BlockRef {
423										height: status.block_height.expect("Confirmed transaction missing block_height"),
424										hash: status.block_hash.expect("Confirmed transaction missing block_hash"),
425									})
426								} else {
427									TxStatus::Mempool
428								}
429							};
430							let txid = output_status.txid.expect("Txid should be valid if an outpoint is spent");
431							res.add(outpoint, txid, tx_status);
432						}
433					}
434				}
435			},
436		}
437
438		Ok(res)
439	}
440
441	pub async fn broadcast_tx(&self, tx: &Transaction) -> anyhow::Result<()> {
442		match self.inner() {
443			ChainSourceClient::Bitcoind(bitcoind) => {
444				match bitcoind.send_raw_transaction(tx) {
445					Ok(_) => Ok(()),
446					Err(rpc::Error::JsonRpc(
447						rpc::jsonrpc::Error::Rpc(e))
448					) if e.code == TX_ALREADY_IN_CHAIN_ERROR => Ok(()),
449					Err(e) => Err(e.into()),
450				}
451			},
452			ChainSourceClient::Esplora(client) => {
453				client.broadcast(tx).await?;
454				Ok(())
455			},
456		}
457	}
458
459	pub async fn broadcast_package(&self, txs: &[impl Borrow<Transaction>]) -> anyhow::Result<()> {
460		#[derive(Debug, Deserialize)]
461		struct PackageTxInfo {
462			txid: Txid,
463			error: Option<String>,
464		}
465		#[derive(Debug, Deserialize)]
466		struct SubmitPackageResponse {
467			#[serde(rename = "tx-results")]
468			tx_results: HashMap<Wtxid, PackageTxInfo>,
469			package_msg: String,
470		}
471
472		match self.inner() {
473			ChainSourceClient::Bitcoind(bitcoind) => {
474				let hexes = txs.iter()
475					.map(|t| bitcoin::consensus::encode::serialize_hex(t.borrow()))
476					.collect::<Vec<_>>();
477				let res = bitcoind.call::<SubmitPackageResponse>("submitpackage", &[hexes.into()])?;
478				if res.package_msg != "success" {
479					let errors = res.tx_results.values()
480						.map(|t| format!("tx {}: {}",
481							t.txid, t.error.as_ref().map(|s| s.as_str()).unwrap_or("(no error)"),
482						))
483						.collect::<Vec<_>>();
484					bail!("msg: '{}', errors: {:?}", res.package_msg, errors);
485				}
486				Ok(())
487			},
488			ChainSourceClient::Esplora(client) => {
489				let txs = txs.iter().map(|t| t.borrow().clone()).collect::<Vec<_>>();
490				let res = client.submit_package(&txs, None, None).await?;
491				if res.package_msg != "success" {
492					let errors = res.tx_results.values()
493						.map(|t| format!("tx {}: {}",
494							t.txid, t.error.as_ref().map(|s| s.as_str()).unwrap_or("(no error)"),
495						))
496						.collect::<Vec<_>>();
497					bail!("msg: '{}', errors: {:?}", res.package_msg, errors);
498				}
499
500				Ok(())
501			},
502		}
503	}
504
505	pub async fn get_tx(&self, txid: &Txid) -> anyhow::Result<Option<Transaction>> {
506		match self.inner() {
507			ChainSourceClient::Bitcoind(bitcoind) => {
508				match bitcoind.get_raw_transaction(txid, None) {
509					Ok(tx) => Ok(Some(tx)),
510					Err(e) if e.is_not_found() => Ok(None),
511					Err(e) => Err(e.into()),
512				}
513			},
514			ChainSourceClient::Esplora(client) => {
515				Ok(client.get_tx(txid).await?)
516			},
517		}
518	}
519
520	/// Returns the block height the tx is confirmed in, if any.
521	pub async fn tx_confirmed(&self, txid: Txid) -> anyhow::Result<Option<BlockHeight>> {
522		Ok(self.tx_status(txid).await?.confirmed_height())
523	}
524
525	/// Returns the status of the given transaction, including the block height if it is confirmed
526	pub async fn tx_status(&self, txid: Txid) -> anyhow::Result<TxStatus> {
527		match self.inner() {
528			ChainSourceClient::Bitcoind(bitcoind) => Ok(bitcoind.tx_status(&txid)?),
529			ChainSourceClient::Esplora(esplora) => {
530				match esplora.get_tx_info(&txid).await? {
531					Some(info) => match (info.status.block_height, info.status.block_hash) {
532						(Some(block_height), Some(block_hash)) => Ok(TxStatus::Confirmed(BlockRef {
533							height: block_height,
534							hash: block_hash,
535						} )),
536						_ => Ok(TxStatus::Mempool),
537					},
538					None => Ok(TxStatus::NotFound),
539				}
540			},
541		}
542	}
543
544	#[allow(unused)]
545	pub async fn txout_value(&self, outpoint: &OutPoint) -> anyhow::Result<Amount> {
546		let tx = match self.inner() {
547			ChainSourceClient::Bitcoind(bitcoind) => {
548				bitcoind.get_raw_transaction(&outpoint.txid, None)
549					.with_context(|| format!("tx {} unknown", outpoint.txid))?
550			},
551			ChainSourceClient::Esplora(client) => {
552				client.get_tx(&outpoint.txid).await?
553					.with_context(|| format!("tx {} unknown", outpoint.txid))?
554			},
555		};
556		Ok(tx.output.get(outpoint.vout as usize).context("outpoint vout out of range")?.value)
557	}
558
559	/// Gets the current fee rates from the chain source, falling back to user-specified values if
560	/// necessary
561	pub async fn update_fee_rates(&self, fallback_fee: Option<FeeRate>) -> anyhow::Result<()> {
562		let fee_rates = match (self.fetch_fee_rates().await, fallback_fee) {
563			(Ok(fee_rates), _) => Ok(fee_rates),
564			(Err(e), None) => Err(e),
565			(Err(e), Some(fallback)) => {
566				warn!("Error getting fee rates, falling back to {} sat/kvB: {}",
567					fallback.to_btc_per_kvb(), e,
568				);
569				Ok(FeeRates { fast: fallback, regular: fallback, slow: fallback })
570			}
571		}?;
572
573		*self.fee_rates.write().await = fee_rates;
574		Ok(())
575	}
576}
577
578/// The [FeeRates] struct represents the fee rates for transactions categorized by speed or urgency.
579#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
580pub struct FeeRates {
581	/// The fee for fast transactions (higher cost, lower time delay).
582	pub fast: FeeRate,
583	/// The fee for standard-priority transactions.
584	pub regular: FeeRate,
585	/// The fee for slower transactions (lower cost, higher time delay).
586	pub slow: FeeRate,
587}
588
589/// Contains the fee information for an unconfirmed transaction found in the mempool.
590#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
591pub struct MempoolAncestorInfo {
592	/// The ID of the transaction that was queried.
593	pub txid: Txid,
594	/// The total fee of this transaction and all of its unconfirmed ancestors. If the transaction
595	/// is to be replaced, the total fees of the published package MUST exceed this.
596	pub total_fee: Amount,
597	/// The total weight of this transaction and all of its unconfirmed ancestors.
598	pub total_weight: Weight,
599}
600
601impl MempoolAncestorInfo {
602	pub fn new(txid: Txid) -> Self {
603		Self {
604			txid,
605			total_fee: Amount::ZERO,
606			total_weight: Weight::ZERO,
607		}
608	}
609
610	pub fn effective_fee_rate(&self) -> Option<FeeRate> {
611		FeeRate::from_amount_and_weight_ceil(self.total_fee, self.total_weight)
612	}
613}
614
615#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
616pub struct TxsSpendingInputsResult {
617	pub map: HashMap<OutPoint, (Txid, TxStatus)>,
618}
619
620impl TxsSpendingInputsResult {
621	pub fn new() -> Self {
622		Self { map: HashMap::new() }
623	}
624
625	pub fn add(&mut self, outpoint: OutPoint, txid: Txid, status: TxStatus) {
626		self.map.insert(outpoint, (txid, status));
627	}
628
629	pub fn get(&self, outpoint: &OutPoint) -> Option<&(Txid, TxStatus)> {
630		self.map.get(outpoint)
631	}
632
633	pub fn confirmed_txids(&self) -> impl Iterator<Item = (Txid, BlockRef)> + '_ {
634		self.map
635			.iter()
636			.filter_map(|(_, (txid, status))| {
637				match status {
638					TxStatus::Confirmed(block) => Some((*txid, *block)),
639					_ => None,
640				}
641			})
642	}
643
644	pub fn mempool_txids(&self) -> impl Iterator<Item = Txid> + '_ {
645		self.map
646			.iter()
647			.filter(|(_, (_, status))| matches!(status, TxStatus::Mempool))
648			.map(|(_, (txid, _))| *txid)
649	}
650}