Skip to main content

fedimint_lnv2_common/
gateway_api.rs

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