fedimint_gw_client/error.rs
1//! Error types of the gateway's LNv1 client module.
2//!
3//! The error types of this module's public operations live here; the payment
4//! errors its state machines persist stay in [`crate::pay`].
5
6use fedimint_api_client::api::FederationError;
7use fedimint_client_module::{AddStateMachinesError, TransactionSubmitError};
8use fedimint_core::core::OperationId;
9use fedimint_core::db::{AutocommitError, DatabaseError};
10use fedimint_lightning::LightningRpcError;
11use fedimint_ln_client::incoming::IncomingSmError;
12use fedimint_ln_common::contracts::ContractId;
13use thiserror::Error;
14
15use crate::UnsafeHtlcExpiry;
16
17/// A failure to handle an HTLC the gateway intercepted.
18///
19/// The gateway buys the payment's preimage from the federation by funding the
20/// incoming contract that the recipient offered. A replay of an HTLC circuit
21/// the gateway already handles is not a failure.
22#[derive(Debug, Error)]
23#[non_exhaustive]
24pub enum HandleInterceptedHtlcError {
25 /// The Lightning node could not report its current block height, which
26 /// the HTLC's expiry is checked against.
27 #[error("The Lightning node's block height could not be read")]
28 BlockHeight(#[source] LightningRpcError),
29
30 /// The HTLC expires too soon for the gateway to settle it safely.
31 #[error("The HTLC expires too soon to be settled safely")]
32 UnsafeExpiry(#[from] UnsafeHtlcExpiry),
33
34 /// The federation's offer for this payment could not be turned into an
35 /// incoming contract: it did not arrive in time, violates the fee
36 /// policy, does not match the HTLC, already has a funded contract, or
37 /// the federation could not be asked.
38 #[error("The incoming contract could not be created")]
39 IncomingContract(#[from] IncomingSmError),
40
41 /// The operation id derived while funding disagrees with the one derived
42 /// from the payment hash. This is a bug in this module; it is reported
43 /// instead of panicking so that the caller can fail the HTLC back.
44 #[error(
45 "Operation id derivation must match: {} != {}",
46 .derived.fmt_short(),
47 .expected.fmt_short()
48 )]
49 OperationIdMismatch {
50 /// The id derived from the payment hash.
51 expected: OperationId,
52 /// The id the funding step derived.
53 derived: OperationId,
54 },
55
56 /// The transaction funding the incoming contract could not be built or
57 /// submitted.
58 #[error("The funding transaction could not be submitted")]
59 Transaction(#[from] TransactionSubmitError),
60}
61
62/// A failure to fund the incoming contract of a direct swap.
63///
64/// A direct swap pays an invoice issued in this federation on behalf of a
65/// client of another federation served by the same gateway, by funding the
66/// invoice's incoming contract here, so the payment never touches the
67/// Lightning network. Joining a swap that is already under way, or declining
68/// to start one, is not a failure.
69#[derive(Debug, Error)]
70#[non_exhaustive]
71pub enum HandleDirectSwapError {
72 /// The federation's offer for this payment could not be turned into an
73 /// incoming contract: it did not arrive in time, violates the fee
74 /// policy, does not match the swap, already has a funded contract, or
75 /// the federation could not be asked.
76 #[error("The incoming contract could not be created")]
77 IncomingContract(#[from] IncomingSmError),
78
79 /// The operation id derived while funding disagrees with the one derived
80 /// from the payment hash. This is a bug in this module.
81 #[error(
82 "Operation id derivation must match: {} != {}",
83 .derived.fmt_short(),
84 .expected.fmt_short()
85 )]
86 OperationIdMismatch {
87 /// The id derived from the payment hash.
88 expected: OperationId,
89 /// The id the funding step derived.
90 derived: OperationId,
91 },
92
93 /// The transaction funding the incoming contract could not be built or
94 /// submitted.
95 #[error("The funding transaction could not be submitted")]
96 Transaction(#[from] TransactionSubmitError),
97
98 /// Recording the swap in the database kept failing.
99 #[error("Database error")]
100 Database(#[from] DatabaseError),
101}
102
103impl From<AutocommitError<HandleDirectSwapError>> for HandleDirectSwapError {
104 fn from(e: AutocommitError<Self>) -> Self {
105 match e {
106 AutocommitError::ClosureError { error, .. } => error,
107 AutocommitError::CommitFailed { last_error, .. } => Self::Database(last_error),
108 }
109 }
110}
111
112/// A failure to start paying an invoice on behalf of a federation client.
113///
114/// These are the refusals that happen before the payment's state machine
115/// starts. Once it runs, its outcome is reported through
116/// [`crate::GatewayClientModule::gateway_subscribe_ln_pay`].
117#[derive(Debug, Error)]
118#[non_exhaustive]
119pub enum GatewayPayInvoiceError {
120 /// The invoice carries no amount, so there is nothing to pay.
121 #[error("Invoice is missing amount")]
122 MissingInvoiceAmount,
123
124 /// The invoice was pruned, and the gateway cannot pay a pruned invoice.
125 #[error("The gateway cannot pay the pruned invoice")]
126 PrunedInvoiceRejected(#[source] GatewayClientV1Error),
127
128 /// Another operation already claimed the invoice's payment hash, and this
129 /// request has no matching funded contract this gateway can refund.
130 #[error("A payment for this payment hash was already accepted")]
131 PaymentHashAlreadyClaimed,
132
133 /// The federation could not return the contract to refund.
134 #[error("Failed to fetch outgoing contract for refund")]
135 ContractLookup(#[source] FederationError),
136
137 /// A payment of this contract is already under way, and the request does
138 /// not carry the authentication it was started with.
139 #[error("Not authorized to receive the preimage for contract {contract_id}")]
140 Unauthorized {
141 /// The contract the request asked to pay.
142 contract_id: ContractId,
143 },
144
145 /// The payment's state machine could not be started.
146 #[error("Failed to add the payment's state machines")]
147 StateMachines(#[source] AddStateMachinesError),
148
149 /// Recording the payment in the database kept failing.
150 #[error("Database error")]
151 Database(#[from] DatabaseError),
152}
153
154impl From<AutocommitError<GatewayPayInvoiceError>> for GatewayPayInvoiceError {
155 fn from(e: AutocommitError<Self>) -> Self {
156 match e {
157 AutocommitError::ClosureError { error, .. } => error,
158 AutocommitError::CommitFailed { last_error, .. } => Self::Database(last_error),
159 }
160 }
161}
162
163/// A failure reported by the gateway behind [`crate::IGatewayClientV1`].
164///
165/// The trait is implemented by the gateway, not by this module, so the causes
166/// are the gateway's own. This type carries them unchanged: its `Display` and
167/// its `source()` are the cause's.
168#[derive(Debug, Error)]
169#[error(transparent)]
170pub struct GatewayClientV1Error(Box<dyn std::error::Error + Send + Sync>);
171
172impl GatewayClientV1Error {
173 /// Wraps a failure of the gateway's [`crate::IGatewayClientV1`]
174 /// implementation, which may be any error value or a plain message.
175 pub fn new<E>(source: E) -> Self
176 where
177 E: Into<Box<dyn std::error::Error + Send + Sync>>,
178 {
179 Self(source.into())
180 }
181}