Skip to main content

breez_sdk_spark/
error.rs

1use crate::{
2    Fee,
3    lnurl::LnurlServerError,
4    persist::{self},
5};
6use bitcoin::consensus::encode::FromHexError;
7use breez_sdk_common::error::ServiceConnectivityError;
8use platform_utils::time::SystemTimeError;
9use serde::{Deserialize, Serialize};
10use spark_wallet::SparkWalletError;
11use std::{convert::Infallible, num::TryFromIntError};
12use thiserror::Error;
13use tracing_subscriber::util::TryInitError;
14
15/// Error type for the `BreezSdk`
16#[derive(Debug, Error, Clone)]
17#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
18pub enum SdkError {
19    #[error("SparkSdkError: {0}")]
20    SparkError(String),
21
22    #[error("Insufficient funds{}", .token_identifier.as_deref().map(|t| format!(" for token {t}")).unwrap_or_default())]
23    InsufficientFunds {
24        /// The token that cannot cover the payment. Unset when the shortfall is
25        /// in sats or when no single token can be named.
26        token_identifier: Option<String>,
27    },
28
29    #[error("Invalid UUID: {0}")]
30    InvalidUuid(String),
31
32    /// Invalid input error
33    #[error("Invalid input: {0}")]
34    InvalidInput(String),
35
36    /// Network error
37    #[error("Network error: {0}")]
38    NetworkError(String),
39
40    /// Storage error
41    #[error("Storage error: {0}")]
42    StorageError(String),
43
44    #[error("Chain service error: {0}")]
45    ChainServiceError(String),
46
47    #[error(
48        "Max deposit claim fee exceeded for utxo: {tx}:{vout} with max fee: {max_fee:?} and required fee: {required_fee_sats} sats or {required_fee_rate_sat_per_vbyte} sats/vbyte"
49    )]
50    MaxDepositClaimFeeExceeded {
51        tx: String,
52        vout: u32,
53        max_fee: Option<Fee>,
54        required_fee_sats: u64,
55        required_fee_rate_sat_per_vbyte: u64,
56    },
57
58    #[error("Missing utxo: {tx}:{vout}")]
59    MissingUtxo { tx: String, vout: u32 },
60
61    #[error("Lnurl error: {0}")]
62    LnurlError(String),
63
64    #[error("Signer error: {0}")]
65    Signer(String),
66
67    /// `optimize_leaves` was called while another optimization run (auto or
68    /// manual) was already in flight.
69    #[error("Optimization is already in progress")]
70    OptimizationAlreadyRunning,
71
72    /// `optimize_leaves` was preempted by the SDK to free leaves for a
73    /// higher-priority operation (typically a payment).
74    #[error("Optimization was cancelled by the SDK to free leaves")]
75    OptimizationCancelled,
76
77    /// The provided CPFP funding is too low to cover the exit's on-chain fees.
78    #[error("Insufficient CPFP funding: need at least {required_sat} sats")]
79    InsufficientCpfpFunds { required_sat: u64 },
80
81    /// A provided funding UTXO was already spent on-chain by a transaction that
82    /// is not the expected fan-out, so it cannot fund this exit.
83    #[error("Funding UTXO {txid}:{vout} was spent by an unrelated transaction")]
84    FundingUtxoConflict { txid: String, vout: u32 },
85
86    #[error("Error: {0}")]
87    Generic(String),
88}
89
90impl From<crate::chain::ChainServiceError> for SdkError {
91    fn from(e: crate::chain::ChainServiceError) -> Self {
92        SdkError::ChainServiceError(e.to_string())
93    }
94}
95
96impl From<breez_sdk_common::lnurl::error::LnurlError> for SdkError {
97    fn from(e: breez_sdk_common::lnurl::error::LnurlError) -> Self {
98        SdkError::LnurlError(e.to_string())
99    }
100}
101
102impl From<breez_sdk_common::input::ParseError> for SdkError {
103    fn from(e: breez_sdk_common::input::ParseError) -> Self {
104        SdkError::InvalidInput(e.to_string())
105    }
106}
107
108impl From<bitcoin::address::ParseError> for SdkError {
109    fn from(e: bitcoin::address::ParseError) -> Self {
110        SdkError::InvalidInput(e.to_string())
111    }
112}
113
114impl From<flashnet::FlashnetError> for SdkError {
115    fn from(e: flashnet::FlashnetError) -> Self {
116        match e {
117            flashnet::FlashnetError::Network { reason, code } => {
118                let code = match code {
119                    Some(c) => format!(" (code: {c})"),
120                    None => String::new(),
121                };
122                SdkError::NetworkError(format!("{reason}{code}"))
123            }
124            _ => SdkError::Generic(e.to_string()),
125        }
126    }
127}
128
129impl From<boltz_client::BoltzError> for SdkError {
130    fn from(e: boltz_client::BoltzError) -> Self {
131        use boltz_client::BoltzError;
132        match e {
133            BoltzError::Api { reason, code } => {
134                let code = match code {
135                    Some(c) => format!(" (code: {c})"),
136                    None => String::new(),
137                };
138                SdkError::NetworkError(format!("Boltz API: {reason}{code}"))
139            }
140            BoltzError::WebSocket(s) => SdkError::NetworkError(format!("Boltz WebSocket: {s}")),
141            BoltzError::Store(s) => SdkError::StorageError(format!("Boltz store: {s}")),
142            BoltzError::AmountOutOfRange { .. }
143            | BoltzError::QuoteExpired
144            | BoltzError::InvalidQuote(_)
145            | BoltzError::QuoteDegradedBeyondSlippage { .. }
146            | BoltzError::DuplicatePreimage => SdkError::InvalidInput(e.to_string()),
147            _ => SdkError::Generic(format!("Boltz: {e}")),
148        }
149    }
150}
151
152impl From<crate::token_conversion::ConversionError> for SdkError {
153    fn from(e: crate::token_conversion::ConversionError) -> Self {
154        use crate::token_conversion::ConversionError;
155        match e {
156            ConversionError::NoPoolsAvailable => {
157                SdkError::Generic("No conversion pools available".to_string())
158            }
159            ConversionError::ConversionFailed(msg)
160            | ConversionError::ValidationFailed(msg)
161            | ConversionError::RefundFailed(msg) => SdkError::Generic(msg),
162            ConversionError::DuplicateTransfer => {
163                SdkError::Generic("Duplicate transfer: conversion already handled".to_string())
164            }
165            ConversionError::Sdk(e) => e,
166            ConversionError::Storage(e) => SdkError::StorageError(e.to_string()),
167            ConversionError::Wallet(e) => SdkError::SparkError(e.to_string()),
168        }
169    }
170}
171
172impl From<persist::StorageError> for SdkError {
173    fn from(e: persist::StorageError) -> Self {
174        match e {
175            persist::StorageError::NotFound => SdkError::InvalidInput("Not found".to_string()),
176            _ => SdkError::StorageError(e.to_string()),
177        }
178    }
179}
180
181impl From<Infallible> for SdkError {
182    fn from(value: Infallible) -> Self {
183        SdkError::Generic(value.to_string())
184    }
185}
186
187impl From<String> for SdkError {
188    fn from(s: String) -> Self {
189        Self::Generic(s)
190    }
191}
192
193impl From<&str> for SdkError {
194    fn from(s: &str) -> Self {
195        Self::Generic(s.to_string())
196    }
197}
198
199impl From<SystemTimeError> for SdkError {
200    fn from(e: SystemTimeError) -> Self {
201        SdkError::Generic(e.to_string())
202    }
203}
204
205impl From<TryFromIntError> for SdkError {
206    fn from(e: TryFromIntError) -> Self {
207        SdkError::Generic(e.to_string())
208    }
209}
210
211impl From<serde_json::Error> for SdkError {
212    fn from(e: serde_json::Error) -> Self {
213        SdkError::Generic(e.to_string())
214    }
215}
216
217impl From<SparkWalletError> for SdkError {
218    fn from(e: SparkWalletError) -> Self {
219        match e {
220            SparkWalletError::InsufficientFunds => SdkError::InsufficientFunds {
221                token_identifier: None,
222            },
223            SparkWalletError::TokenOutputServiceError(
224                spark_wallet::TokenOutputServiceError::InsufficientFunds { token_identifier },
225            ) => SdkError::InsufficientFunds { token_identifier },
226            SparkWalletError::ServiceError(spark_wallet::ServiceError::InvalidInput(msg)) => {
227                SdkError::InvalidInput(msg)
228            }
229            SparkWalletError::ServiceError(
230                spark_wallet::ServiceError::InsufficientCpfpBudget { required_sat },
231            ) => SdkError::InsufficientCpfpFunds { required_sat },
232            SparkWalletError::ServiceError(spark_wallet::ServiceError::FundingUtxoConflict {
233                txid,
234                vout,
235            }) => SdkError::FundingUtxoConflict { txid, vout },
236            _ => SdkError::SparkError(e.to_string()),
237        }
238    }
239}
240
241impl From<spark_wallet::OptimizationError> for SdkError {
242    fn from(e: spark_wallet::OptimizationError) -> Self {
243        match e {
244            spark_wallet::OptimizationError::AlreadyRunning => SdkError::OptimizationAlreadyRunning,
245            spark_wallet::OptimizationError::Cancelled => SdkError::OptimizationCancelled,
246            spark_wallet::OptimizationError::Tree(inner) => SdkError::SparkError(inner.to_string()),
247        }
248    }
249}
250
251impl From<FromHexError> for SdkError {
252    fn from(e: FromHexError) -> Self {
253        SdkError::Generic(e.to_string())
254    }
255}
256
257impl From<uuid::Error> for SdkError {
258    fn from(e: uuid::Error) -> Self {
259        SdkError::InvalidUuid(e.to_string())
260    }
261}
262
263impl From<ServiceConnectivityError> for SdkError {
264    fn from(value: ServiceConnectivityError) -> Self {
265        SdkError::NetworkError(value.to_string())
266    }
267}
268
269impl From<LnurlServerError> for SdkError {
270    fn from(value: LnurlServerError) -> Self {
271        match value {
272            LnurlServerError::InvalidApiKey => {
273                SdkError::InvalidInput("Invalid api key".to_string())
274            }
275            LnurlServerError::Network {
276                statuscode,
277                message,
278            } => SdkError::NetworkError(format!(
279                "network request failed with status {statuscode}: {}",
280                message.unwrap_or(String::new())
281            )),
282            LnurlServerError::RequestFailure(e) => SdkError::NetworkError(e),
283            LnurlServerError::SigningError(e) => {
284                SdkError::Generic(format!("Failed to sign message: {e}"))
285            }
286        }
287    }
288}
289
290impl From<TryInitError> for SdkError {
291    fn from(_value: TryInitError) -> Self {
292        SdkError::Generic("Logging can only be initialized once".to_string())
293    }
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, Error, PartialEq)]
297#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
298pub enum DepositClaimError {
299    #[error(
300        "Max deposit claim fee exceeded for utxo: {tx}:{vout} with max fee: {max_fee:?} and required fee: {required_fee_sats} sats or {required_fee_rate_sat_per_vbyte} sats/vbyte"
301    )]
302    MaxDepositClaimFeeExceeded {
303        tx: String,
304        vout: u32,
305        max_fee: Option<Fee>,
306        required_fee_sats: u64,
307        required_fee_rate_sat_per_vbyte: u64,
308    },
309
310    #[error("Missing utxo: {tx}:{vout}")]
311    MissingUtxo { tx: String, vout: u32 },
312
313    #[error("Generic error: {message}")]
314    Generic { message: String },
315}
316
317impl From<SdkError> for DepositClaimError {
318    fn from(value: SdkError) -> Self {
319        match value {
320            SdkError::MaxDepositClaimFeeExceeded {
321                tx,
322                vout,
323                max_fee,
324                required_fee_sats,
325                required_fee_rate_sat_per_vbyte,
326            } => DepositClaimError::MaxDepositClaimFeeExceeded {
327                tx,
328                vout,
329                max_fee,
330                required_fee_sats,
331                required_fee_rate_sat_per_vbyte,
332            },
333            SdkError::MissingUtxo { tx, vout } => DepositClaimError::MissingUtxo { tx, vout },
334            SdkError::Generic(e) => DepositClaimError::Generic { message: e },
335            _ => DepositClaimError::Generic {
336                message: value.to_string(),
337            },
338        }
339    }
340}
341
342/// Error type for signer operations
343#[derive(Debug, Error, Clone)]
344#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
345pub enum SignerError {
346    #[error("Key derivation error: {0}")]
347    KeyDerivation(String),
348
349    #[error("Signing error: {0}")]
350    Signing(String),
351
352    #[error("Encryption error: {0}")]
353    Encryption(String),
354
355    #[error("Decryption error: {0}")]
356    Decryption(String),
357
358    #[error("Encryption unavailable: {0}")]
359    EncryptionUnavailable(String),
360
361    #[error("FROST error: {0}")]
362    Frost(String),
363
364    #[error("Invalid input: {0}")]
365    InvalidInput(String),
366
367    #[error("Generic signer error: {0}")]
368    Generic(String),
369}
370
371impl From<String> for SignerError {
372    fn from(s: String) -> Self {
373        SignerError::Generic(s)
374    }
375}
376
377impl From<&str> for SignerError {
378    fn from(s: &str) -> Self {
379        SignerError::Generic(s.to_string())
380    }
381}