Skip to main content

fedimint_lnv2_common/
gateway_api.rs

1use std::str::FromStr;
2
3use bitcoin::secp256k1::PublicKey;
4use bitcoin::secp256k1::schnorr::Signature;
5use fedimint_connectors::error::ServerError;
6use fedimint_core::config::FederationId;
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::util::SafeUrl;
9use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send};
10use fedimint_ln_common::client::GatewayApi;
11use lightning_invoice::{Bolt11Invoice, RoutingFees};
12use reqwest::Method;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::contracts::{IncomingContract, OutgoingContract};
17use crate::endpoint_constants::{
18    CREATE_BOLT11_INVOICE_ENDPOINT, ROUTING_INFO_ENDPOINT, SEND_PAYMENT_ENDPOINT,
19};
20use crate::{Bolt11InvoiceDescription, LightningInvoice};
21
22#[apply(async_trait_maybe_send!)]
23pub trait GatewayConnection: std::fmt::Debug {
24    async fn routing_info(
25        &self,
26        gateway_api: SafeUrl,
27        federation_id: &FederationId,
28    ) -> Result<Option<RoutingInfo>, ServerError>;
29
30    async fn bolt11_invoice(
31        &self,
32        gateway_api: SafeUrl,
33        federation_id: FederationId,
34        contract: IncomingContract,
35        amount: Amount,
36        description: Bolt11InvoiceDescription,
37        expiry_secs: u32,
38    ) -> Result<Bolt11Invoice, ServerError>;
39
40    async fn send_payment(
41        &self,
42        gateway_api: SafeUrl,
43        federation_id: FederationId,
44        outpoint: OutPoint,
45        contract: OutgoingContract,
46        invoice: LightningInvoice,
47        auth: Signature,
48    ) -> Result<Result<[u8; 32], Signature>, ServerError>;
49}
50
51#[derive(Debug, Clone)]
52pub struct RealGatewayConnection {
53    pub api: GatewayApi,
54}
55
56#[apply(async_trait_maybe_send!)]
57impl GatewayConnection for RealGatewayConnection {
58    async fn routing_info(
59        &self,
60        gateway_api: SafeUrl,
61        federation_id: &FederationId,
62    ) -> Result<Option<RoutingInfo>, ServerError> {
63        self.api
64            .request(
65                &gateway_api,
66                Method::POST,
67                ROUTING_INFO_ENDPOINT,
68                Some(federation_id),
69            )
70            .await
71    }
72
73    async fn bolt11_invoice(
74        &self,
75        gateway_api: SafeUrl,
76        federation_id: FederationId,
77        contract: IncomingContract,
78        amount: Amount,
79        description: Bolt11InvoiceDescription,
80        expiry_secs: u32,
81    ) -> Result<Bolt11Invoice, ServerError> {
82        self.api
83            .request(
84                &gateway_api,
85                Method::POST,
86                CREATE_BOLT11_INVOICE_ENDPOINT,
87                Some(CreateBolt11InvoicePayload {
88                    federation_id,
89                    contract,
90                    amount,
91                    description,
92                    expiry_secs,
93                }),
94            )
95            .await
96    }
97
98    async fn send_payment(
99        &self,
100        gateway_api: SafeUrl,
101        federation_id: FederationId,
102        outpoint: OutPoint,
103        contract: OutgoingContract,
104        invoice: LightningInvoice,
105        auth: Signature,
106    ) -> Result<Result<[u8; 32], Signature>, ServerError> {
107        self.api
108            .request(
109                &gateway_api,
110                Method::POST,
111                SEND_PAYMENT_ENDPOINT,
112                Some(SendPaymentPayload {
113                    federation_id,
114                    outpoint,
115                    contract,
116                    invoice,
117                    auth,
118                }),
119            )
120            .await
121    }
122}
123
124/// The maximum invoice expiry a client may request via
125/// `create_bolt11_invoice`. Bounding the expiry bounds the lifetime of both
126/// the hold invoice created on the gateway's Lightning node and the incoming
127/// contract record in the gateway's database.
128pub const MAX_INVOICE_EXPIRY_SECS: u32 = 60 * 60 * 24;
129
130#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
131pub struct CreateBolt11InvoicePayload {
132    pub federation_id: FederationId,
133    pub contract: IncomingContract,
134    pub amount: Amount,
135    pub description: Bolt11InvoiceDescription,
136    pub expiry_secs: u32,
137}
138
139#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
140pub struct SendPaymentPayload {
141    pub federation_id: FederationId,
142    pub outpoint: OutPoint,
143    pub contract: OutgoingContract,
144    pub invoice: LightningInvoice,
145    pub auth: Signature,
146}
147
148#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
149pub struct RoutingInfo {
150    /// The public key of the gateways lightning node. Since this key signs the
151    /// gateways invoices the senders client uses it to differentiate between a
152    /// direct swap between fedimints and a lightning swap.
153    pub lightning_public_key: PublicKey,
154    /// The human-readable alias of the gateway's lightning node, if available.
155    ///
156    /// This field is optional for backwards-compatibility with older gateways
157    /// that do not yet provide an alias in their `routing_info` responses.
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub lightning_alias: Option<String>,
160    /// The public key of the gateways client module. This key is used to claim
161    /// or cancel outgoing contracts and refund incoming contracts.
162    pub module_public_key: PublicKey,
163    /// This is the fee the gateway charges for an outgoing payment. The senders
164    /// client will use this fee in case of a direct swap.
165    pub send_fee_minimum: PaymentFee,
166    /// This is the default total fee the gateway recommends for an outgoing
167    /// payment in case of a lightning swap. It accounts for the additional fee
168    /// required to reliably route this payment over lightning.
169    pub send_fee_default: PaymentFee,
170    /// This is the minimum expiration delta in block the gateway requires for
171    /// an outgoing payment. The senders client will use this expiration delta
172    /// in case of a direct swap.
173    pub expiration_delta_minimum: u64,
174    /// This is the default total expiration the gateway recommends for an
175    /// outgoing payment in case of a lightning swap. It accounts for the
176    /// additional expiration delta required to successfully route this payment
177    /// over lightning.
178    pub expiration_delta_default: u64,
179    /// This is the fee the gateway charges for an incoming payment.
180    pub receive_fee: PaymentFee,
181}
182
183impl RoutingInfo {
184    pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
185        if invoice.recover_payee_pub_key() == self.lightning_public_key {
186            (self.send_fee_minimum, self.expiration_delta_minimum)
187        } else {
188            (self.send_fee_default, self.expiration_delta_default)
189        }
190    }
191}
192
193#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable, Copy)]
194pub struct PaymentFee {
195    pub base: Amount,
196    pub parts_per_million: u64,
197}
198
199impl PaymentFee {
200    /// This is the maximum send fee of one and a half percent plus one hundred
201    /// satoshis a correct gateway may recommend as a default. It accounts for
202    /// the fee required to reliably route this payment over lightning.
203    pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
204        base: Amount::from_sats(100),
205        parts_per_million: 15_000,
206    };
207
208    /// This is the fee the gateway uses to cover transaction fees with the
209    /// federation.
210    pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
211        base: Amount::from_sats(2),
212        parts_per_million: 3000,
213    };
214
215    /// This is the maximum receive fee of half of one percent plus fifty
216    /// satoshis a correct gateway may recommend as a default.
217    pub const RECEIVE_FEE_LIMIT: PaymentFee = PaymentFee {
218        base: Amount::from_sats(50),
219        parts_per_million: 5_000,
220    };
221
222    /// Returns `true` if this fee is within `limit` in both components.
223    ///
224    /// `absolute_fee` is monotonically increasing in `base` and in
225    /// `parts_per_million`, so a fee is bounded by the limit only when neither
226    /// component exceeds it. This is intentionally a named method rather than a
227    /// derived `PartialOrd`, which orders the fields lexicographically and
228    /// therefore stops at `base` whenever the two bases differ.
229    pub fn is_within(&self, limit: &PaymentFee) -> bool {
230        self.base <= limit.base && self.parts_per_million <= limit.parts_per_million
231    }
232
233    /// Adds two fees, returning `None` if either component overflows.
234    ///
235    /// Fees reach this method straight from operator input, so the addition
236    /// has to happen before the limit checks can run. A panicking `Add` would
237    /// therefore be reachable with a fee that the limits are meant to reject.
238    pub fn checked_add(self, rhs: Self) -> Option<PaymentFee> {
239        Some(PaymentFee {
240            base: self.base.checked_add(rhs.base)?,
241            parts_per_million: self.parts_per_million.checked_add(rhs.parts_per_million)?,
242        })
243    }
244
245    pub fn add_to(&self, msats: u64) -> Amount {
246        Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
247    }
248
249    pub fn subtract_from(&self, msats: u64) -> Amount {
250        Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
251    }
252
253    pub fn fee(&self, msats: u64) -> Amount {
254        Amount::from_msats(self.absolute_fee(msats))
255    }
256
257    fn absolute_fee(&self, msats: u64) -> u64 {
258        // The base fee is bounded well below `u64::MAX` for any fee that passed
259        // the limit checks, but a fee decoded from an older database has not
260        // necessarily passed them, so saturate rather than panic.
261        msats
262            .saturating_mul(self.parts_per_million)
263            .saturating_div(1_000_000)
264            .saturating_add(self.base.msats)
265    }
266}
267
268/// A [`PaymentFee`] that does not fit into the `u32` components of
269/// [`RoutingFees`] and therefore cannot be announced to lightning clients.
270#[derive(Debug, Error)]
271#[error("Payment fee {0} exceeds the range of RoutingFees")]
272pub struct FeeOutOfRangeError(PaymentFee);
273
274impl From<RoutingFees> for PaymentFee {
275    fn from(value: RoutingFees) -> Self {
276        PaymentFee {
277            base: Amount::from_msats(u64::from(value.base_msat)),
278            parts_per_million: u64::from(value.proportional_millionths),
279        }
280    }
281}
282
283impl TryFrom<PaymentFee> for RoutingFees {
284    type Error = FeeOutOfRangeError;
285
286    fn try_from(value: PaymentFee) -> Result<Self, Self::Error> {
287        Ok(RoutingFees {
288            base_msat: u32::try_from(value.base.msats).map_err(|_| FeeOutOfRangeError(value))?,
289            proportional_millionths: u32::try_from(value.parts_per_million)
290                .map_err(|_| FeeOutOfRangeError(value))?,
291        })
292    }
293}
294
295impl std::fmt::Display for PaymentFee {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        write!(f, "{},{}", self.base, self.parts_per_million)
298    }
299}
300
301impl FromStr for PaymentFee {
302    type Err = anyhow::Error;
303
304    fn from_str(s: &str) -> Result<Self, Self::Err> {
305        let mut parts = s.split(',');
306        let base_str = parts
307            .next()
308            .ok_or(anyhow::anyhow!("Failed to parse base fee"))?;
309        let ppm_str = parts.next().ok_or(anyhow::anyhow!("Failed to parse ppm"))?;
310
311        // Ensure no extra parts
312        if parts.next().is_some() {
313            return Err(anyhow::anyhow!(
314                "Failed to parse fees. Expected format <base>,<ppm>"
315            ));
316        }
317
318        let base = Amount::from_str(base_str)?;
319        let parts_per_million = ppm_str.parse::<u64>()?;
320
321        Ok(PaymentFee {
322            base,
323            parts_per_million,
324        })
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use fedimint_core::Amount;
331    use lightning_invoice::RoutingFees;
332
333    use super::PaymentFee;
334
335    /// A lower `base` must not let an over-limit `parts_per_million` through,
336    /// which is what the lexicographic ordering used to allow.
337    #[test]
338    fn is_within_enforces_both_components() {
339        // base under the limit, ppm over it, on the send side.
340        let over_ppm = PaymentFee {
341            base: Amount::from_sats(0),
342            parts_per_million: 1_000_000,
343        };
344        assert!(!over_ppm.is_within(&PaymentFee::SEND_FEE_LIMIT));
345
346        let over_ppm = PaymentFee {
347            base: Amount::from_sats(0),
348            parts_per_million: 5_000_000,
349        };
350        assert!(!over_ppm.is_within(&PaymentFee::SEND_FEE_LIMIT));
351
352        // Same on the receive side, which has the lower cap of the two.
353        let over_ppm = PaymentFee {
354            base: Amount::from_sats(0),
355            parts_per_million: 500_000,
356        };
357        assert!(!over_ppm.is_within(&PaymentFee::RECEIVE_FEE_LIMIT));
358
359        // The reverse case, which the ordering already rejected.
360        let over_base = PaymentFee {
361            base: Amount::from_sats(101),
362            parts_per_million: 0,
363        };
364        assert!(!over_base.is_within(&PaymentFee::SEND_FEE_LIMIT));
365
366        // Exactly at the limit is accepted.
367        assert!(PaymentFee::SEND_FEE_LIMIT.is_within(&PaymentFee::SEND_FEE_LIMIT));
368
369        // Strictly within on both components is accepted.
370        let ok = PaymentFee {
371            base: Amount::from_sats(50),
372            parts_per_million: 10_000,
373        };
374        assert!(ok.is_within(&PaymentFee::SEND_FEE_LIMIT));
375    }
376
377    /// Adding two operator-supplied fees must not panic on overflow, in either
378    /// component.
379    #[test]
380    fn checked_add_reports_overflow_instead_of_panicking() {
381        let max_base = PaymentFee {
382            base: Amount::from_msats(u64::MAX),
383            parts_per_million: 0,
384        };
385        let one_msat = PaymentFee {
386            base: Amount::from_msats(1),
387            parts_per_million: 0,
388        };
389        assert!(max_base.checked_add(one_msat).is_none());
390
391        let max_ppm = PaymentFee {
392            base: Amount::ZERO,
393            parts_per_million: u64::MAX,
394        };
395        let one_ppm = PaymentFee {
396            base: Amount::ZERO,
397            parts_per_million: 1,
398        };
399        assert!(max_ppm.checked_add(one_ppm).is_none());
400
401        assert_eq!(
402            PaymentFee::TRANSACTION_FEE_DEFAULT
403                .checked_add(PaymentFee::TRANSACTION_FEE_DEFAULT)
404                .expect("Two default fees fit"),
405            PaymentFee {
406                base: Amount::from_sats(4),
407                parts_per_million: 6_000,
408            }
409        );
410    }
411
412    /// A fee that outgrew the `u32`s of `RoutingFees` must surface as an error
413    /// rather than a panic: the conversion runs on every lightning payment and
414    /// on federation registration at startup, so a panic here boot-loops the
415    /// gateway.
416    #[test]
417    fn routing_fees_conversion_rejects_out_of_range_fees() {
418        let over_base = PaymentFee {
419            base: Amount::from_msats(u64::from(u32::MAX) + 1),
420            parts_per_million: 0,
421        };
422        assert!(RoutingFees::try_from(over_base).is_err());
423
424        let over_ppm = PaymentFee {
425            base: Amount::ZERO,
426            parts_per_million: u64::from(u32::MAX) + 1,
427        };
428        assert!(RoutingFees::try_from(over_ppm).is_err());
429
430        let fees = RoutingFees::try_from(PaymentFee::SEND_FEE_LIMIT)
431            .expect("The send fee limit is within range");
432        assert_eq!(fees.base_msat, 100_000);
433        assert_eq!(fees.proportional_millionths, 15_000);
434    }
435
436    /// Any fee that `set_fees` accepts is small enough for the fee arithmetic
437    /// to stay exact, and an out-of-range fee left in an old database must
438    /// saturate instead of panicking.
439    #[test]
440    fn absolute_fee_saturates_instead_of_panicking() {
441        let huge = PaymentFee {
442            base: Amount::from_msats(u64::MAX),
443            parts_per_million: u64::MAX,
444        };
445        assert_eq!(huge.fee(u64::MAX), Amount::from_msats(u64::MAX));
446
447        assert_eq!(
448            PaymentFee::TRANSACTION_FEE_DEFAULT.fee(1_000_000),
449            Amount::from_msats(2_000 + 3_000)
450        );
451    }
452}