Skip to main content

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}