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