Skip to main content

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 human-readable alias of the gateway's lightning node, if available.
149    ///
150    /// This field is optional for backwards-compatibility with older gateways
151    /// that do not yet provide an alias in their `routing_info` responses.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub lightning_alias: Option<String>,
154    /// The public key of the gateways client module. This key is used to claim
155    /// or cancel outgoing contracts and refund incoming contracts.
156    pub module_public_key: PublicKey,
157    /// This is the fee the gateway charges for an outgoing payment. The senders
158    /// client will use this fee in case of a direct swap.
159    pub send_fee_minimum: PaymentFee,
160    /// This is the default total fee the gateway recommends for an outgoing
161    /// payment in case of a lightning swap. It accounts for the additional fee
162    /// required to reliably route this payment over lightning.
163    pub send_fee_default: PaymentFee,
164    /// This is the minimum expiration delta in block the gateway requires for
165    /// an outgoing payment. The senders client will use this expiration delta
166    /// in case of a direct swap.
167    pub expiration_delta_minimum: u64,
168    /// This is the default total expiration the gateway recommends for an
169    /// outgoing payment in case of a lightning swap. It accounts for the
170    /// additional expiration delta required to successfully route this payment
171    /// over lightning.
172    pub expiration_delta_default: u64,
173    /// This is the fee the gateway charges for an incoming payment.
174    pub receive_fee: PaymentFee,
175}
176
177impl RoutingInfo {
178    pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
179        if invoice.recover_payee_pub_key() == self.lightning_public_key {
180            (self.send_fee_minimum, self.expiration_delta_minimum)
181        } else {
182            (self.send_fee_default, self.expiration_delta_default)
183        }
184    }
185}
186
187#[derive(
188    Debug,
189    Clone,
190    Eq,
191    PartialEq,
192    PartialOrd,
193    Hash,
194    Serialize,
195    Deserialize,
196    Encodable,
197    Decodable,
198    Copy,
199)]
200pub struct PaymentFee {
201    pub base: Amount,
202    pub parts_per_million: u64,
203}
204
205impl PaymentFee {
206    /// This is the maximum send fee of one and a half percent plus one hundred
207    /// satoshis a correct gateway may recommend as a default. It accounts for
208    /// the fee required to reliably route this payment over lightning.
209    pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
210        base: Amount::from_sats(100),
211        parts_per_million: 15_000,
212    };
213
214    /// This is the fee the gateway uses to cover transaction fees with the
215    /// federation.
216    pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
217        base: Amount::from_sats(2),
218        parts_per_million: 3000,
219    };
220
221    /// This is the maximum receive fee of half of one percent plus fifty
222    /// satoshis a correct gateway may recommend as a default.
223    pub const RECEIVE_FEE_LIMIT: PaymentFee = PaymentFee {
224        base: Amount::from_sats(50),
225        parts_per_million: 5_000,
226    };
227
228    pub fn add_to(&self, msats: u64) -> Amount {
229        Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
230    }
231
232    pub fn subtract_from(&self, msats: u64) -> Amount {
233        Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
234    }
235
236    pub fn fee(&self, msats: u64) -> Amount {
237        Amount::from_msats(self.absolute_fee(msats))
238    }
239
240    fn absolute_fee(&self, msats: u64) -> u64 {
241        msats
242            .saturating_mul(self.parts_per_million)
243            .saturating_div(1_000_000)
244            .checked_add(self.base.msats)
245            .expect("The division creates sufficient headroom to add the base fee")
246    }
247}
248
249impl Add for PaymentFee {
250    type Output = PaymentFee;
251    fn add(self, rhs: Self) -> Self::Output {
252        PaymentFee {
253            base: self.base + rhs.base,
254            parts_per_million: self.parts_per_million + rhs.parts_per_million,
255        }
256    }
257}
258
259impl From<RoutingFees> for PaymentFee {
260    fn from(value: RoutingFees) -> Self {
261        PaymentFee {
262            base: Amount::from_msats(u64::from(value.base_msat)),
263            parts_per_million: u64::from(value.proportional_millionths),
264        }
265    }
266}
267
268impl From<PaymentFee> for RoutingFees {
269    fn from(value: PaymentFee) -> Self {
270        RoutingFees {
271            base_msat: u32::try_from(value.base.msats).expect("base msat was truncated from u64"),
272            proportional_millionths: u32::try_from(value.parts_per_million)
273                .expect("ppm was truncated from u64"),
274        }
275    }
276}
277
278impl std::fmt::Display for PaymentFee {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        write!(f, "{},{}", self.base, self.parts_per_million)
281    }
282}
283
284impl FromStr for PaymentFee {
285    type Err = anyhow::Error;
286
287    fn from_str(s: &str) -> Result<Self, Self::Err> {
288        let mut parts = s.split(',');
289        let base_str = parts
290            .next()
291            .ok_or(anyhow::anyhow!("Failed to parse base fee"))?;
292        let ppm_str = parts.next().ok_or(anyhow::anyhow!("Failed to parse ppm"))?;
293
294        // Ensure no extra parts
295        if parts.next().is_some() {
296            return Err(anyhow::anyhow!(
297                "Failed to parse fees. Expected format <base>,<ppm>"
298            ));
299        }
300
301        let base = Amount::from_str(base_str)?;
302        let parts_per_million = ppm_str.parse::<u64>()?;
303
304        Ok(PaymentFee {
305            base,
306            parts_per_million,
307        })
308    }
309}