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#[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
352pub(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
360pub(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}