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::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}