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}