fedimint_lnv2_common/
gateway_api.rs

1use std::ops::Add;
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, apply, async_trait_maybe_send};
11use fedimint_ln_common::client::GatewayApi;
12use lightning_invoice::{Bolt11Invoice, RoutingFees};
13use reqwest::Method;
14use serde::{Deserialize, Serialize};
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#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
125pub struct CreateBolt11InvoicePayload {
126    pub federation_id: FederationId,
127    pub contract: IncomingContract,
128    pub amount: Amount,
129    pub description: Bolt11InvoiceDescription,
130    pub expiry_secs: u32,
131}
132
133#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
134pub struct SendPaymentPayload {
135    pub federation_id: FederationId,
136    pub outpoint: OutPoint,
137    pub contract: OutgoingContract,
138    pub invoice: LightningInvoice,
139    pub auth: Signature,
140}
141
142#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
143pub struct RoutingInfo {
144    /// The public key of the gateways lightning node. Since this key signs the
145    /// gateways invoices the senders client uses it to differentiate between a
146    /// direct swap between fedimints and a lightning swap.
147    pub lightning_public_key: PublicKey,
148    /// The public key of the gateways client module. This key is used to claim
149    /// or cancel outgoing contracts and refund incoming contracts.
150    pub module_public_key: PublicKey,
151    /// This is the fee the gateway charges for an outgoing payment. The senders
152    /// client will use this fee in case of a direct swap.
153    pub send_fee_minimum: PaymentFee,
154    /// This is the default total fee the gateway recommends for an outgoing
155    /// payment in case of a lightning swap. It accounts for the additional fee
156    /// required to reliably route this payment over lightning.
157    pub send_fee_default: PaymentFee,
158    /// This is the minimum expiration delta in block the gateway requires for
159    /// an outgoing payment. The senders client will use this expiration delta
160    /// in case of a direct swap.
161    pub expiration_delta_minimum: u64,
162    /// This is the default total expiration the gateway recommends for an
163    /// outgoing payment in case of a lightning swap. It accounts for the
164    /// additional expiration delta required to successfully route this payment
165    /// over lightning.
166    pub expiration_delta_default: u64,
167    /// This is the fee the gateway charges for an incoming payment.
168    pub receive_fee: PaymentFee,
169}
170
171impl RoutingInfo {
172    pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
173        if invoice.recover_payee_pub_key() == self.lightning_public_key {
174            (self.send_fee_minimum, self.expiration_delta_minimum)
175        } else {
176            (self.send_fee_default, self.expiration_delta_default)
177        }
178    }
179}
180
181#[derive(
182    Debug,
183    Clone,
184    Eq,
185    PartialEq,
186    PartialOrd,
187    Hash,
188    Serialize,
189    Deserialize,
190    Encodable,
191    Decodable,
192    Copy,
193)]
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    pub fn add_to(&self, msats: u64) -> Amount {
223        Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
224    }
225
226    pub fn subtract_from(&self, msats: u64) -> Amount {
227        Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
228    }
229
230    pub fn fee(&self, msats: u64) -> Amount {
231        Amount::from_msats(self.absolute_fee(msats))
232    }
233
234    fn absolute_fee(&self, msats: u64) -> u64 {
235        msats
236            .saturating_mul(self.parts_per_million)
237            .saturating_div(1_000_000)
238            .checked_add(self.base.msats)
239            .expect("The division creates sufficient headroom to add the base fee")
240    }
241}
242
243impl Add for PaymentFee {
244    type Output = PaymentFee;
245    fn add(self, rhs: Self) -> Self::Output {
246        PaymentFee {
247            base: self.base + rhs.base,
248            parts_per_million: self.parts_per_million + rhs.parts_per_million,
249        }
250    }
251}
252
253impl From<RoutingFees> for PaymentFee {
254    fn from(value: RoutingFees) -> Self {
255        PaymentFee {
256            base: Amount::from_msats(u64::from(value.base_msat)),
257            parts_per_million: u64::from(value.proportional_millionths),
258        }
259    }
260}
261
262impl From<PaymentFee> for RoutingFees {
263    fn from(value: PaymentFee) -> Self {
264        RoutingFees {
265            base_msat: u32::try_from(value.base.msats).expect("base msat was truncated from u64"),
266            proportional_millionths: u32::try_from(value.parts_per_million)
267                .expect("ppm was truncated from u64"),
268        }
269    }
270}
271
272impl std::fmt::Display for PaymentFee {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        write!(f, "{},{}", self.base, self.parts_per_million)
275    }
276}
277
278impl FromStr for PaymentFee {
279    type Err = anyhow::Error;
280
281    fn from_str(s: &str) -> Result<Self, Self::Err> {
282        let mut parts = s.split(',');
283        let base_str = parts
284            .next()
285            .ok_or(anyhow::anyhow!("Failed to parse base fee"))?;
286        let ppm_str = parts.next().ok_or(anyhow::anyhow!("Failed to parse ppm"))?;
287
288        // Ensure no extra parts
289        if parts.next().is_some() {
290            return Err(anyhow::anyhow!(
291                "Failed to parse fees. Expected format <base>,<ppm>"
292            ));
293        }
294
295        let base = Amount::from_str(base_str)?;
296        let parts_per_million = ppm_str.parse::<u64>()?;
297
298        Ok(PaymentFee {
299            base,
300            parts_per_million,
301        })
302    }
303}