fedimint_ln_client/error.rs
1//! Error types of the lightning client.
2//!
3//! Every failure this module reports to its callers is named here, so there is
4//! one place for an integrator to look.
5
6use fedimint_api_client::api::{FederationError, ServerError};
7use fedimint_client_module::error::{
8 OperationAlreadyExistsError, OperationLookupError, TransactionSubmitError,
9};
10use fedimint_core::core::OperationId;
11use fedimint_core::db::DatabaseError;
12use fedimint_core::secp256k1::PublicKey;
13#[cfg(feature = "uniffi")]
14use fedimint_core::util::FmtCompact as _;
15use fedimint_core::{Amount, secp256k1};
16use fedimint_ln_common::contracts::ContractId;
17use lightning_invoice::{CreationError, Currency, ParseOrSemanticError};
18use thiserror::Error;
19
20use crate::incoming::IncomingSmError;
21
22/// A failure to pick a Lightning gateway for an operation.
23///
24/// Choosing a gateway is one question with two entry points: pick the best
25/// available one, or look a specific one up. Both can end without a usable
26/// answer, either because the federation has no registrations at all or
27/// because the ones it has do not respond.
28#[derive(Debug, Error)]
29#[non_exhaustive]
30pub enum GatewaySelectionError {
31 /// The caller named a gateway and that gateway did not answer.
32 #[error("Gateway {gateway_id} is offline")]
33 Offline {
34 /// The gateway the caller asked for.
35 gateway_id: PublicKey,
36 },
37
38 /// No gateway is registered with the federation, so there is nothing to
39 /// choose from.
40 #[error("No gateway is registered with the federation")]
41 NoGatewaysRegistered,
42
43 /// Gateways are registered, but none of them answered.
44 #[error("No registered gateway was reachable")]
45 NoneReachable,
46
47 /// The gateway cache could not be refreshed from the federation, so there
48 /// is no up-to-date list to choose from.
49 #[error("The gateway cache could not be refreshed")]
50 Federation(#[source] Box<FederationError>),
51}
52
53impl From<FederationError> for GatewaySelectionError {
54 fn from(source: FederationError) -> Self {
55 Self::Federation(Box::new(source))
56 }
57}
58
59#[cfg(feature = "uniffi")]
60impl From<GatewaySelectionError> for fedimint_core::util::ffi::UniffiError {
61 fn from(e: GatewaySelectionError) -> Self {
62 Self::General(e.fmt_compact().to_string())
63 }
64}
65
66/// A failure to work out the largest invoice amount the client could pay in
67/// full.
68#[derive(Debug, Error)]
69#[non_exhaustive]
70pub enum SpendableAmountError {
71 /// No gateway could be chosen, so there is no fee schedule to compute
72 /// against.
73 #[error("No gateway could be selected for the payment")]
74 Gateway(#[from] GatewaySelectionError),
75
76 /// Gateway selection succeeded but returned nothing.
77 ///
78 /// This is defensive: the selection this method performs always either
79 /// yields a gateway or fails, so no caller is expected to see it.
80 #[error("No gateway is available to send the payment")]
81 NoGatewayAvailable,
82
83 /// The balance cannot cover the smallest payable amount plus the gateway
84 /// and federation fees, so there is no amount to send.
85 #[error("The balance {balance} is too low to send any amount after fees")]
86 BalanceTooLow {
87 /// The balance the answer was computed against.
88 balance: Amount,
89 },
90
91 /// The fee probe failed for a reason unrelated to the balance.
92 #[error("The fee quote for the payment failed")]
93 Quote(#[source] TransactionSubmitError),
94}
95
96/// A failure to follow a lightning operation.
97///
98/// Every entry point that takes an operation id and reports on it, the pay,
99/// receive, claim and recurring-receive subscriptions, the payment-detail
100/// lookup and the outgoing-payment await, first has to find the operation and
101/// then check that it is the kind of lightning operation being asked about.
102/// Both halves are named here, so a caller can tell "I have never seen that
103/// operation" from "that operation is a receive, not a payment".
104#[derive(Debug, Error)]
105#[non_exhaustive]
106pub enum LnSubscribeError {
107 /// The operation could not be looked up, or belongs to another module.
108 #[error("The lightning operation could not be looked up")]
109 Operation(#[from] OperationLookupError),
110
111 /// The operation belongs to the lightning module, but it is not an
112 /// outgoing payment.
113 #[error("The operation is not a lightning payment")]
114 NotAPayment,
115
116 /// The operation belongs to the lightning module, but it is not a receive.
117 #[error("The operation is not a lightning receive")]
118 NotAReceive,
119
120 /// The operation belongs to the lightning module, but it is not a claim of
121 /// an already-funded incoming contract.
122 #[error("The operation is not a lightning claim")]
123 NotAClaim,
124
125 /// The operation belongs to the lightning module, but it is not a receive
126 /// against a recurring payment code.
127 #[error("The operation is not a recurring lightning receive")]
128 NotARecurringReceive,
129
130 /// The operation is a payment that goes out over Lightning rather than
131 /// settling inside the federation, so it has no internal payment states.
132 #[error("The operation is an external lightning payment, not an internal one")]
133 NotInternalPayment,
134
135 /// The operation is a payment, but it is settled inside the federation, so
136 /// it has no Lightning payment states.
137 #[error("The operation is an internal lightning payment, not an external one")]
138 NotExternalPayment,
139
140 /// The payment's update stream ended without reaching a final state.
141 #[error("The outgoing lightning payment did not reach a final state")]
142 NoFinalState,
143}
144
145#[cfg(feature = "uniffi")]
146impl From<LnSubscribeError> for fedimint_core::util::ffi::UniffiError {
147 fn from(e: LnSubscribeError) -> Self {
148 Self::General(e.fmt_compact().to_string())
149 }
150}
151
152/// A failure to pay a BOLT11 invoice.
153///
154/// The first three variants predate this type's move into this module and are
155/// the conditions a caller most often has to react to: an attempt that is
156/// still running, no gateway to route through, and a contract that someone has
157/// already funded for this payment hash. The rest name what used to be folded
158/// into one opaque message: the invoice itself being unusable, the gateway or
159/// the federation refusing, and the transaction failing to submit.
160#[derive(Debug, Error)]
161#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
162#[cfg_attr(feature = "uniffi", uniffi(flat_error))]
163#[non_exhaustive]
164pub enum PayBolt11InvoiceError {
165 /// An earlier attempt to pay this same invoice has not finished.
166 #[error("Previous payment attempt({}) still in progress", .operation_id.fmt_full())]
167 PreviousPaymentAttemptStillInProgress {
168 /// The operation the earlier attempt runs under.
169 operation_id: OperationId,
170 },
171
172 /// The payment has to go out over Lightning and no gateway was supplied.
173 #[error("No LN gateway available")]
174 NoLnGatewayAvailable,
175
176 /// A contract for this payment hash is already funded, so funding another
177 /// would pay twice.
178 #[error("Funded contract already exists: {}", .contract_id)]
179 FundedContractAlreadyExists {
180 /// The contract that already holds funds.
181 contract_id: ContractId,
182 },
183
184 /// The invoice's expiry has passed, so the recipient will not accept the
185 /// payment.
186 #[error("The invoice has expired")]
187 InvoiceExpired,
188
189 /// The invoice is for a different chain than this federation runs on.
190 #[error("The invoice is for {found:?}, but this federation is on {expected:?}")]
191 WrongCurrency {
192 /// The currency this federation's network implies.
193 expected: Currency,
194 /// The currency the invoice names.
195 found: Currency,
196 },
197
198 /// The invoice carries no amount, so there is nothing to lock into a
199 /// contract.
200 #[error("The invoice does not specify an amount")]
201 MissingInvoiceAmount,
202
203 /// The chosen gateway did not answer, so funding a contract for it would
204 /// lock money up with nobody to claim it.
205 #[error("The gateway is not available")]
206 GatewayUnavailable(#[source] ServerError),
207
208 /// The federation did not report a consensus block count, so the
209 /// contract's timelock cannot be computed.
210 #[error("The federation did not report a consensus block count")]
211 NoConsensusBlockCount,
212
213 /// A request to the federation failed.
214 #[error("The federation request failed")]
215 Federation(#[source] Box<FederationError>),
216
217 /// The internal (federation-settled) contract for this payment could not
218 /// be built.
219 #[error("The internal payment contract could not be created")]
220 InternalContract(#[source] IncomingSmError),
221
222 /// This client's internal-payment markers could not be derived, so an
223 /// internal payment cannot be recognised.
224 #[error("The internal payment markers could not be derived")]
225 PaymentMarkers(#[source] secp256k1::Error),
226
227 /// The caller's extra metadata could not be serialized into the operation
228 /// log.
229 #[error("The extra metadata could not be serialized")]
230 ExtraMeta(#[source] serde_json::Error),
231
232 /// The payment attempt could not be written to the database.
233 #[error("Database error")]
234 Database(#[from] DatabaseError),
235
236 /// The transaction funding the payment could not be built or submitted.
237 #[error("The payment transaction could not be submitted")]
238 Transaction(#[from] TransactionSubmitError),
239}
240
241impl From<FederationError> for PayBolt11InvoiceError {
242 fn from(source: FederationError) -> Self {
243 Self::Federation(Box::new(source))
244 }
245}
246
247#[cfg(feature = "uniffi")]
248impl From<PayBolt11InvoiceError> for fedimint_core::util::ffi::UniffiError {
249 fn from(e: PayBolt11InvoiceError) -> Self {
250 Self::General(e.fmt_compact().to_string())
251 }
252}
253
254/// A failure to create a BOLT11 invoice to be paid into this federation.
255#[derive(Debug, Error)]
256#[non_exhaustive]
257pub enum CreateBolt11InvoiceError {
258 /// This client's internal-payment markers could not be derived, so the
259 /// invoice cannot be built for an internal payment.
260 #[error("The internal payment markers could not be derived")]
261 PaymentMarkers(#[source] secp256k1::Error),
262
263 /// The invoice could not be assembled from the parameters given.
264 #[error("The invoice could not be built")]
265 InvoiceCreation(#[source] CreationError),
266
267 /// The transaction publishing the offer could not be built or submitted.
268 #[error("The offer transaction could not be submitted")]
269 Transaction(#[from] TransactionSubmitError),
270
271 /// The federation rejected the transaction publishing the offer, so
272 /// nothing would be able to pay the invoice.
273 ///
274 /// The payload is the message the submission recorded rather than an error
275 /// value, so it is part of this error's own message.
276 #[error("The offer transaction was rejected: {reason}")]
277 OfferRejected {
278 /// What the submission reported.
279 reason: String,
280 },
281}
282
283#[cfg(feature = "uniffi")]
284impl From<CreateBolt11InvoiceError> for fedimint_core::util::ffi::UniffiError {
285 fn from(e: CreateBolt11InvoiceError) -> Self {
286 Self::General(e.fmt_compact().to_string())
287 }
288}
289
290/// A failure to claim an incoming contract the federation already holds.
291///
292/// This is the deprecated pre-recurring-payments receive path: a client that
293/// knows the key an invoice was issued against goes looking for the contract
294/// funded under it and spends it.
295#[derive(Debug, Error)]
296#[non_exhaustive]
297pub enum ClaimIncomingContractError {
298 /// The federation holds no funded contract under this id, so there is
299 /// nothing to claim.
300 #[error("No funded contract exists for {contract_id}")]
301 ContractNotFound {
302 /// The contract that was looked for.
303 contract_id: ContractId,
304 },
305
306 /// The contract could not be fetched from the federation.
307 #[error("The contract could not be fetched")]
308 Federation(#[source] Box<FederationError>),
309
310 /// The transaction claiming the contract could not be built or submitted.
311 #[error("The claim transaction could not be submitted")]
312 Transaction(#[from] TransactionSubmitError),
313}
314
315impl From<FederationError> for ClaimIncomingContractError {
316 fn from(source: FederationError) -> Self {
317 Self::Federation(Box::new(source))
318 }
319}
320
321/// A failure to restart the claim of an already-paid lightning invoice.
322///
323/// This is a break-glass recovery tool, so most of its refusals are about the
324/// original operation not being in a state that can be reclaimed.
325#[derive(Debug, Error)]
326#[non_exhaustive]
327pub enum ReclaimLnReceiveError {
328 /// The original operation could not be looked up, or belongs to another
329 /// module.
330 #[error("The original operation could not be looked up")]
331 Operation(#[from] OperationLookupError),
332
333 /// The original operation's metadata could not be read, which normally
334 /// means an earlier database migration left it in a shape this version
335 /// does not understand.
336 #[error("The lightning operation metadata could not be read")]
337 Meta(#[source] serde_json::Error),
338
339 /// The original operation is a lightning operation, but not one of the
340 /// receives a reclaim can restart.
341 #[error("The operation is not a reclaimable lightning receive")]
342 NotReclaimable,
343
344 /// The original receive still has running state machines, so it is
345 /// already trying to claim and a second attempt would race it.
346 #[error("The lightning receive is still active")]
347 StillActive,
348
349 /// The key the invoice was issued against is not in this client's state
350 /// history, so the contract cannot be spent.
351 #[error("The original receive key is not available in the local state history")]
352 ReceiveKeyUnavailable,
353
354 /// An operation for the reclaim attempt already exists.
355 #[error("The reclaim operation already exists")]
356 OperationAlreadyExists(#[from] OperationAlreadyExistsError),
357}
358
359/// A failure to turn user input into a BOLT11 invoice to pay.
360///
361/// Covers both halves of the path: working out whether the input is an
362/// invoice, an LNURL or a lightning address, and then obtaining the invoice
363/// that input stands for.
364#[derive(Debug, Error)]
365#[non_exhaustive]
366pub enum PaymentInfoError {
367 /// The input is neither a BOLT11 invoice, an LNURL nor a lightning
368 /// address.
369 ///
370 /// The source is the invoice parser's complaint, which is the most
371 /// informative of the three attempts.
372 #[error("The input is not an invoice, an LNURL or a lightning address")]
373 NotAnInvoiceOrLnurl(#[source] ParseOrSemanticError),
374
375 /// The LNURL endpoint could not be reached, or answered with something
376 /// that is not a valid LNURL response.
377 #[error("The LNURL request failed")]
378 Lnurl(#[source] lnurl::Error),
379
380 /// The LNURL resolved, but it is not a pay request, so there is nothing to
381 /// pay.
382 #[error("The LNURL is not a pay request")]
383 NotAPayRequest,
384
385 /// The invoice already carries an amount and one was given on the command
386 /// line, so it is not clear which was meant.
387 #[error("The amount is specified both in the invoice and separately")]
388 AmountInInvoiceAndCommandLine,
389
390 /// The invoice carries no amount, which this client does not support.
391 #[error("The invoice does not specify an amount")]
392 AmountMissingFromInvoice,
393
394 /// An LNURL names no amount of its own, so one has to be supplied.
395 #[error("An amount must be specified when paying to an LNURL")]
396 AmountRequiredForLnurl,
397
398 /// The LNURL endpoint answered with something that is not a BOLT11
399 /// invoice.
400 #[error("The LNURL endpoint did not return a valid invoice")]
401 InvoiceParse(#[source] ParseOrSemanticError),
402
403 /// The LNURL endpoint returned an invoice for a different amount than the
404 /// one that was requested.
405 #[error("The LNURL returned an invoice for {generated:?} instead of the requested {requested}")]
406 AmountMismatch {
407 /// The amount that was asked for.
408 requested: Amount,
409 /// The amount the returned invoice carries.
410 generated: Option<Amount>,
411 },
412
413 /// The input looked like an LNURL or a lightning address but could not be
414 /// decoded.
415 #[error("The LNURL or lightning address could not be decoded")]
416 LnurlDecode(#[source] lnurl::Error),
417}
418
419#[cfg(feature = "uniffi")]
420impl From<PaymentInfoError> for fedimint_core::util::ffi::UniffiError {
421 fn from(e: PaymentInfoError) -> Self {
422 Self::General(e.fmt_compact().to_string())
423 }
424}