Skip to main content

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}