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