Skip to main content

breez_sdk_liquid/
error.rs

1use anyhow::Error;
2use log::warn;
3use lwk_wollet::secp256k1;
4use sdk_common::{
5    lightning_with_bolt12::offers::parse::Bolt12SemanticError,
6    prelude::{LnUrlAuthError, LnUrlPayError, LnUrlWithdrawError},
7};
8
9use crate::payjoin::error::PayjoinError;
10
11pub type SdkResult<T, E = SdkError> = Result<T, E>;
12
13#[macro_export]
14macro_rules! ensure_sdk {
15    ($cond:expr, $err:expr) => {
16        if !$cond {
17            return Err($err);
18        }
19    };
20}
21
22// TODO Unify error enum
23#[derive(Debug, thiserror::Error)]
24pub enum SdkError {
25    #[error("Liquid SDK instance is already running")]
26    AlreadyStarted,
27
28    #[error("Error: {err}")]
29    Generic { err: String },
30
31    #[error("Network {network} is not currently supported")]
32    NetworkNotSupported { network: String },
33
34    #[error("Liquid SDK instance is not running")]
35    NotStarted,
36
37    #[error("Service connectivity: {err}")]
38    ServiceConnectivity { err: String },
39}
40impl SdkError {
41    pub fn generic<T: AsRef<str>>(err: T) -> Self {
42        Self::Generic {
43            err: err.as_ref().to_string(),
44        }
45    }
46
47    pub(crate) fn network_not_supported<T: ToString>(network: T) -> Self {
48        Self::NetworkNotSupported {
49            network: network.to_string(),
50        }
51    }
52}
53
54impl From<anyhow::Error> for SdkError {
55    fn from(e: Error) -> Self {
56        SdkError::generic(e.to_string())
57    }
58}
59
60impl From<boltz_client::error::Error> for SdkError {
61    fn from(err: boltz_client::error::Error) -> Self {
62        match err {
63            boltz_client::error::Error::HTTP(e) => {
64                SdkError::generic(format!("Could not contact servers: {e:?}"))
65            }
66            boltz_client::error::Error::HTTPStatusNotSuccess(status, body) => {
67                SdkError::generic(format!("Boltz API returned error status {status}: {body}"))
68            }
69            _ => SdkError::generic(format!("{err:?}")),
70        }
71    }
72}
73
74impl From<secp256k1::Error> for SdkError {
75    fn from(err: secp256k1::Error) -> Self {
76        SdkError::generic(format!("{err:?}"))
77    }
78}
79
80#[derive(thiserror::Error, Debug)]
81pub enum PaymentError {
82    #[error("The specified funds have already been claimed")]
83    AlreadyClaimed,
84
85    #[error("The specified funds have already been sent")]
86    AlreadyPaid,
87
88    #[error("The payment is already in progress")]
89    PaymentInProgress,
90
91    #[error("Amount must be between {min} and {max}")]
92    AmountOutOfRange { min: u64, max: u64 },
93
94    #[error("Amount is missing: {err}")]
95    AmountMissing { err: String },
96
97    #[error("Asset error: {err}")]
98    AssetError { err: String },
99
100    #[error("Invalid network: {err}")]
101    InvalidNetwork { err: String },
102
103    #[error("Generic error: {err}")]
104    Generic { err: String },
105
106    #[error("The provided fees have expired")]
107    InvalidOrExpiredFees,
108
109    #[error("Cannot pay: not enough funds")]
110    InsufficientFunds,
111
112    #[error("Invalid description: {err}")]
113    InvalidDescription { err: String },
114
115    #[error("The specified invoice is not valid: {err}")]
116    InvalidInvoice { err: String },
117
118    #[error("The generated preimage is not valid")]
119    InvalidPreimage,
120
121    #[error("Boltz did not return any pairs from the request")]
122    PairsNotFound,
123
124    #[error("Payment start could not be verified within the configured timeout")]
125    PaymentTimeout,
126
127    #[error("Could not store the swap details locally")]
128    PersistError,
129
130    #[error("Could not process the Receive Payment: {err}")]
131    ReceiveError { err: String },
132
133    #[error("The payment has been refunded. Reason for failure: {err}")]
134    Refunded { err: String, refund_tx_id: String },
135
136    #[error("The payment is a self-transfer, which is not supported")]
137    SelfTransferNotSupported,
138
139    #[error("Could not process the Send Payment: {err}")]
140    SendError { err: String },
141
142    #[error("Could not sign the transaction: {err}")]
143    SignerError { err: String },
144}
145impl PaymentError {
146    pub(crate) fn asset_error<S: AsRef<str>>(err: S) -> Self {
147        Self::AssetError {
148            err: err.as_ref().to_string(),
149        }
150    }
151
152    pub(crate) fn generic<S: AsRef<str>>(err: S) -> Self {
153        Self::Generic {
154            err: err.as_ref().to_string(),
155        }
156    }
157
158    pub(crate) fn invalid_invoice<S: AsRef<str>>(err: S) -> Self {
159        Self::InvalidInvoice {
160            err: err.as_ref().to_string(),
161        }
162    }
163
164    pub(crate) fn invalid_network<S: AsRef<str>>(err: S) -> Self {
165        Self::InvalidNetwork {
166            err: err.as_ref().to_string(),
167        }
168    }
169
170    pub(crate) fn receive_error<S: AsRef<str>>(err: S) -> Self {
171        Self::ReceiveError {
172            err: err.as_ref().to_string(),
173        }
174    }
175
176    pub(crate) fn amount_missing<S: AsRef<str>>(err: S) -> Self {
177        Self::AmountMissing {
178            err: err.as_ref().to_string(),
179        }
180    }
181}
182
183impl From<Bolt12SemanticError> for PaymentError {
184    fn from(err: Bolt12SemanticError) -> Self {
185        PaymentError::Generic {
186            err: format!("Failed to create BOLT12 invoice: {err:?}"),
187        }
188    }
189}
190
191impl From<boltz_client::error::Error> for PaymentError {
192    fn from(err: boltz_client::error::Error) -> Self {
193        match err {
194            boltz_client::error::Error::HTTP(e) => PaymentError::Generic {
195                err: format!("Could not contact servers: {e:?}"),
196            },
197            boltz_client::error::Error::HTTPStatusNotSuccess(status, body) => {
198                PaymentError::Generic {
199                    err: format!("Boltz API returned error status {status}: {body}"),
200                }
201            }
202            _ => PaymentError::Generic {
203                err: format!("{err:?}"),
204            },
205        }
206    }
207}
208
209impl From<boltz_client::bitcoin::hex::HexToArrayError> for PaymentError {
210    fn from(err: boltz_client::bitcoin::hex::HexToArrayError) -> Self {
211        PaymentError::Generic {
212            err: format!("{err:?}"),
213        }
214    }
215}
216
217impl From<lwk_wollet::Error> for PaymentError {
218    fn from(err: lwk_wollet::Error) -> Self {
219        match err {
220            lwk_wollet::Error::InsufficientFunds {
221                missing_sats,
222                asset_id,
223                is_token,
224            } => {
225                warn!(
226                    "lwk reported insufficient funds: missing {missing_sats} sat of asset \
227                     {asset_id}, is_token: {is_token}"
228                );
229                PaymentError::InsufficientFunds
230            }
231            lwk_wollet::Error::TooManyInputs(count) => PaymentError::Generic {
232                err: format!(
233                    "Transaction would require {count} inputs, which exceeds the maximum of 256 \
234                     supported by the Liquid surjection proof. Consolidate UTXOs and try again."
235                ),
236            },
237            _ => PaymentError::Generic {
238                err: format!("{err:?}"),
239            },
240        }
241    }
242}
243
244#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
245impl From<lwk_wollet::UrlError> for PaymentError {
246    fn from(err: lwk_wollet::UrlError) -> Self {
247        PaymentError::Generic {
248            err: format!("{err:?}"),
249        }
250    }
251}
252
253impl From<lwk_signer::SignerError> for PaymentError {
254    fn from(err: lwk_signer::SignerError) -> Self {
255        PaymentError::SignerError {
256            err: format!("{err:?}"),
257        }
258    }
259}
260
261impl From<anyhow::Error> for PaymentError {
262    fn from(err: anyhow::Error) -> Self {
263        Self::Generic {
264            err: err.to_string(),
265        }
266    }
267}
268
269impl From<PayjoinError> for PaymentError {
270    fn from(err: PayjoinError) -> Self {
271        match err {
272            PayjoinError::InsufficientFunds => PaymentError::InsufficientFunds,
273            _ => PaymentError::Generic {
274                err: format!("{err:?}"),
275            },
276        }
277    }
278}
279
280impl From<rusqlite::Error> for PaymentError {
281    fn from(err: rusqlite::Error) -> Self {
282        log::error!("Persister returned error: {err:?}");
283        Self::PersistError
284    }
285}
286
287impl From<SdkError> for PaymentError {
288    fn from(err: SdkError) -> Self {
289        Self::Generic {
290            err: err.to_string(),
291        }
292    }
293}
294
295impl From<sdk_common::bitcoin::util::bip32::Error> for PaymentError {
296    fn from(err: sdk_common::bitcoin::util::bip32::Error) -> Self {
297        Self::SignerError {
298            err: err.to_string(),
299        }
300    }
301}
302
303impl From<secp256k1::Error> for PaymentError {
304    fn from(err: secp256k1::Error) -> Self {
305        Self::Generic {
306            err: err.to_string(),
307        }
308    }
309}
310
311impl From<PaymentError> for LnUrlAuthError {
312    fn from(err: PaymentError) -> Self {
313        Self::Generic {
314            err: err.to_string(),
315        }
316    }
317}
318
319impl From<PaymentError> for LnUrlPayError {
320    fn from(err: PaymentError) -> Self {
321        match err {
322            PaymentError::AlreadyPaid => Self::AlreadyPaid,
323            PaymentError::AmountOutOfRange { min, max } => Self::InvalidAmount {
324                err: format!("Amount must be between {min} and {max}"),
325            },
326            PaymentError::AmountMissing { err } => Self::InvalidAmount {
327                err: format!("Amount is missing: {err}"),
328            },
329            PaymentError::InvalidNetwork { err } => Self::InvalidNetwork { err },
330            PaymentError::InsufficientFunds => Self::InsufficientBalance { err: String::new() },
331            PaymentError::InvalidInvoice { err } => Self::InvalidInvoice { err },
332            PaymentError::PaymentTimeout => Self::PaymentTimeout { err: String::new() },
333            _ => Self::Generic {
334                err: err.to_string(),
335            },
336        }
337    }
338}
339
340impl From<PaymentError> for LnUrlWithdrawError {
341    fn from(err: PaymentError) -> Self {
342        Self::Generic {
343            err: err.to_string(),
344        }
345    }
346}
347
348pub(crate) fn is_txn_mempool_conflict_error(err: &Error) -> bool {
349    err.to_string().contains("txn-mempool-conflict")
350}
351
352/// Whether an input is absent from the node's UTXO set: already spent by a confirmed tx, or its
353/// parent never existed. Unlike [`is_txn_mempool_conflict_error`], which means we merely compete
354/// with an unconfirmed tx, this means our cached UTXO view is stale.
355pub(crate) fn is_txn_inputs_missing_or_spent_error(err: &Error) -> bool {
356    let err = err.to_string();
357    err.contains("bad-txns-inputs-missingorspent") || err.contains("missing-inputs")
358}
359
360/// Whether the output being spent is already claimed by another tx, whether that tx is in the
361/// mempool (`txn-mempool-conflict`) or confirmed (`bad-txns-inputs-missingorspent`).
362pub(crate) fn is_txn_already_spent_error(err: &Error) -> bool {
363    is_txn_mempool_conflict_error(err) || is_txn_inputs_missing_or_spent_error(err)
364}