Skip to main content

psbt_v2/v2/
extract.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! Implementation of the Extractor role as defined in [BIP-174].
4//!
5//! # Extractor Role
6//!
7//! > The Transaction Extractor does not need to know how to interpret scripts in order
8//! > to extract the network serialized transaction.
9//!
10//! It is only possible to extract a transaction from a PSBT _after_ it has been finalized. However
11//! the Extractor role may be fulfilled by a separate entity to the Finalizer hence this is a
12//! separate module and does not require the "miniscript" feature be enabled.
13//!
14//! [BIP-174]: <https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki>
15
16use core::fmt;
17
18use bitcoin::{FeeRate, Transaction, Txid};
19
20use crate::error::{write_err, FeeError};
21use crate::v2::{DetermineLockTimeError, Psbt};
22
23/// Implements the BIP-370 Finalized role.
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub struct Extractor(Psbt);
27
28impl Extractor {
29    /// Creates an `Extractor`.
30    ///
31    /// An extractor can only accept a PSBT that has been finalized.
32    pub fn new(psbt: Psbt) -> Result<Self, ExtractError> {
33        if psbt.inputs.iter().any(|input| !input.is_finalized()) {
34            return Err(ExtractError::PsbtNotFinalized);
35        }
36        let _ = psbt.determine_lock_time()?;
37
38        Ok(Self(psbt))
39    }
40
41    /// Returns this PSBT's unique identification.
42    pub fn id(&self) -> Txid {
43        self.0.id().expect("Extractor guarantees lock time can be determined")
44    }
45}
46
47impl Extractor {
48    /// The default `max_fee_rate` value used for extracting transactions with [`Self::extract_tx`].
49    ///
50    /// As of 2023, even the biggest overpayers during the highest fee markets only paid around
51    /// 1000 sats/vByte. 25k sats/vByte is obviously a mistake at this point.
52    pub const DEFAULT_MAX_FEE_RATE: FeeRate = FeeRate::from_sat_per_vb_unchecked(25_000);
53
54    /// An alias for [`Self::extract_tx_fee_rate_limit`].
55    pub fn extract_tx(&self) -> Result<Transaction, ExtractTxFeeRateError> {
56        self.internal_extract_tx_with_fee_rate_limit(Self::DEFAULT_MAX_FEE_RATE)
57    }
58
59    /// Extracts the [`Transaction`] from a [`Psbt`] by filling in the available signature information.
60    ///
61    /// ## Errors
62    ///
63    /// `ExtractTxError` variants will contain either the [`Psbt`] itself or the [`Transaction`]
64    /// that was extracted. These can be extracted from the Errors in order to recover.
65    /// See the error documentation for info on the variants. In general, it covers large fees.
66    pub fn extract_tx_fee_rate_limit(&self) -> Result<Transaction, ExtractTxFeeRateError> {
67        self.internal_extract_tx_with_fee_rate_limit(Self::DEFAULT_MAX_FEE_RATE)
68    }
69
70    /// Extracts the [`Transaction`] from a [`Psbt`] by filling in the available signature information.
71    pub fn extract_tx_with_fee_rate_limit(
72        &self,
73        max_fee_rate: FeeRate,
74    ) -> Result<Transaction, ExtractTxFeeRateError> {
75        self.internal_extract_tx_with_fee_rate_limit(max_fee_rate)
76    }
77
78    /// Perform [`Self::extract_tx_fee_rate_limit`] without the fee rate check.
79    ///
80    /// This can result in a transaction with absurdly high fees. Use with caution.
81    #[allow(clippy::result_large_err)]
82    pub fn extract_tx_unchecked_fee_rate(&self) -> Result<Transaction, ExtractTxError> {
83        self.internal_extract_tx()
84    }
85
86    #[inline]
87    fn internal_extract_tx_with_fee_rate_limit(
88        &self,
89        max_fee_rate: FeeRate,
90    ) -> Result<Transaction, ExtractTxFeeRateError> {
91        let fee = self.0.fee()?;
92        let tx = self.internal_extract_tx()?;
93
94        // Now that the extracted Transaction is made, decide how to return it.
95        let fee_rate =
96            FeeRate::from_sat_per_kwu(fee.to_sat().saturating_mul(1000) / tx.weight().to_wu());
97        // Prefer to return an AbsurdFeeRate error when both trigger.
98        if fee_rate > max_fee_rate {
99            return Err(ExtractTxFeeRateError::FeeTooHigh { fee: fee_rate, max: max_fee_rate });
100        }
101
102        Ok(tx)
103    }
104
105    /// Extracts a finalized transaction from the [`Psbt`].
106    ///
107    /// Uses `miniscript` to do interpreter checks.
108    #[inline]
109    #[allow(clippy::result_large_err)]
110    fn internal_extract_tx(&self) -> Result<Transaction, ExtractTxError> {
111        if !self.0.is_finalized() {
112            return Err(ExtractTxError::Unfinalized);
113        }
114
115        let lock_time = self.0.determine_lock_time()?;
116
117        let tx = Transaction {
118            version: self.0.global.tx_version,
119            lock_time,
120            input: self.0.inputs.iter().map(|input| input.signed_tx_in()).collect(),
121            output: self.0.outputs.iter().map(|ouput| ouput.tx_out()).collect(),
122        };
123
124        Ok(tx)
125    }
126}
127
128/// Error constructing an `Extractor`.
129#[derive(Debug)]
130pub enum ExtractError {
131    /// Attempted to extract tx from an unfinalized PSBT.
132    PsbtNotFinalized,
133    /// Finalizer must be able to determine the lock time.
134    DetermineLockTime(DetermineLockTimeError),
135}
136
137impl fmt::Display for ExtractError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        use ExtractError::*;
140
141        match *self {
142            PsbtNotFinalized => write!(f, "attempted to extract tx from an unfinalized PSBT"),
143            DetermineLockTime(ref e) =>
144                write_err!(f, "extractor must be able to determine the lock time"; e),
145        }
146    }
147}
148
149#[cfg(feature = "std")]
150impl std::error::Error for ExtractError {
151    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
152        use ExtractError::*;
153
154        match *self {
155            DetermineLockTime(ref e) => Some(e),
156            PsbtNotFinalized => None,
157        }
158    }
159}
160
161impl From<DetermineLockTimeError> for ExtractError {
162    fn from(e: DetermineLockTimeError) -> Self { Self::DetermineLockTime(e) }
163}
164
165/// Error caused by fee calculation when extracting a [`Transaction`] from a PSBT.
166#[derive(Debug, Clone, PartialEq, Eq)]
167#[non_exhaustive]
168pub enum ExtractTxFeeRateError {
169    /// Error calculating the fee rate.
170    Fee(FeeError),
171    /// The calculated fee rate exceeds max.
172    FeeTooHigh {
173        /// Calculated fee.
174        fee: FeeRate,
175        /// Maximum allowable fee.
176        max: FeeRate,
177    },
178    /// Error extracting the transaction.
179    ExtractTx(ExtractTxError),
180}
181
182impl fmt::Display for ExtractTxFeeRateError {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        use ExtractTxFeeRateError::*;
185
186        match *self {
187            Fee(ref e) => write_err!(f, "fee calculation"; e),
188            FeeTooHigh { fee, max } => write!(f, "fee {} is greater than max {}", fee, max),
189            ExtractTx(ref e) => write_err!(f, "extract"; e),
190        }
191    }
192}
193
194#[cfg(feature = "std")]
195impl std::error::Error for ExtractTxFeeRateError {
196    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
197        use ExtractTxFeeRateError::*;
198
199        match *self {
200            Fee(ref e) => Some(e),
201            ExtractTx(ref e) => Some(e),
202            FeeTooHigh { .. } => None,
203        }
204    }
205}
206
207impl From<FeeError> for ExtractTxFeeRateError {
208    fn from(e: FeeError) -> Self { Self::Fee(e) }
209}
210
211impl From<ExtractTxError> for ExtractTxFeeRateError {
212    fn from(e: ExtractTxError) -> Self { Self::ExtractTx(e) }
213}
214
215/// Error extracting a [`Transaction`] from a PSBT.
216#[derive(Debug, Clone, PartialEq, Eq)]
217#[non_exhaustive]
218pub enum ExtractTxError {
219    /// Attempted to extract transaction from an unfinalized PSBT.
220    Unfinalized,
221    /// Failed to determine lock time.
222    DetermineLockTime(DetermineLockTimeError),
223}
224
225impl fmt::Display for ExtractTxError {
226    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!() }
227}
228
229#[cfg(feature = "std")]
230impl std::error::Error for ExtractTxError {
231    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { todo!() }
232}
233
234impl From<DetermineLockTimeError> for ExtractTxError {
235    fn from(e: DetermineLockTimeError) -> Self { Self::DetermineLockTime(e) }
236}