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_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 lightning_invoice::{Bolt11Invoice, RoutingFees};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use crate::contracts::{IncomingContract, OutgoingContract};
15use crate::endpoint_constants::{
16    CREATE_BOLT11_INVOICE_ENDPOINT, ROUTING_INFO_ENDPOINT, SEND_PAYMENT_ENDPOINT,
17};
18use crate::{Bolt11InvoiceDescription, LightningInvoice};
19
20#[apply(async_trait_maybe_send!)]
21pub trait GatewayConnection: std::fmt::Debug {
22    async fn routing_info(
23        &self,
24        gateway_api: SafeUrl,
25        federation_id: &FederationId,
26    ) -> Result<Option<RoutingInfo>, GatewayConnectionError>;
27
28    async fn bolt11_invoice(
29        &self,
30        gateway_api: SafeUrl,
31        federation_id: FederationId,
32        contract: IncomingContract,
33        amount: Amount,
34        description: Bolt11InvoiceDescription,
35        expiry_secs: u32,
36    ) -> Result<Bolt11Invoice, GatewayConnectionError>;
37
38    async fn send_payment(
39        &self,
40        gateway_api: SafeUrl,
41        federation_id: FederationId,
42        outpoint: OutPoint,
43        contract: OutgoingContract,
44        invoice: LightningInvoice,
45        auth: Signature,
46    ) -> Result<Result<[u8; 32], Signature>, GatewayConnectionError>;
47}
48
49#[derive(Error, Debug, Clone, Eq, PartialEq)]
50pub enum GatewayConnectionError {
51    #[error("The gateway is unreachable: {0}")]
52    Unreachable(String),
53    #[error("The gateway returned an error for this request: {0}")]
54    Request(String),
55}
56
57#[derive(Debug)]
58pub struct RealGatewayConnection;
59
60#[apply(async_trait_maybe_send!)]
61impl GatewayConnection for RealGatewayConnection {
62    async fn routing_info(
63        &self,
64        gateway_api: SafeUrl,
65        federation_id: &FederationId,
66    ) -> Result<Option<RoutingInfo>, GatewayConnectionError> {
67        reqwest::Client::new()
68            .post(
69                gateway_api
70                    .join(ROUTING_INFO_ENDPOINT)
71                    .expect("'routing_info' contains no invalid characters for a URL")
72                    .as_str(),
73            )
74            .json(federation_id)
75            .send()
76            .await
77            .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
78            .json::<Option<RoutingInfo>>()
79            .await
80            .map_err(|e| GatewayConnectionError::Request(e.to_string()))
81    }
82
83    async fn bolt11_invoice(
84        &self,
85        gateway_api: SafeUrl,
86        federation_id: FederationId,
87        contract: IncomingContract,
88        amount: Amount,
89        description: Bolt11InvoiceDescription,
90        expiry_secs: u32,
91    ) -> Result<Bolt11Invoice, GatewayConnectionError> {
92        reqwest::Client::new()
93            .post(
94                gateway_api
95                    .join(CREATE_BOLT11_INVOICE_ENDPOINT)
96                    .expect("'create_bolt11_invoice' contains no invalid characters for a URL")
97                    .as_str(),
98            )
99            .json(&CreateBolt11InvoicePayload {
100                federation_id,
101                contract,
102                amount,
103                description,
104                expiry_secs,
105            })
106            .send()
107            .await
108            .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
109            .json::<Bolt11Invoice>()
110            .await
111            .map_err(|e| GatewayConnectionError::Request(e.to_string()))
112    }
113
114    async fn send_payment(
115        &self,
116        gateway_api: SafeUrl,
117        federation_id: FederationId,
118        outpoint: OutPoint,
119        contract: OutgoingContract,
120        invoice: LightningInvoice,
121        auth: Signature,
122    ) -> Result<Result<[u8; 32], Signature>, GatewayConnectionError> {
123        reqwest::Client::new()
124            .post(
125                gateway_api
126                    .join(SEND_PAYMENT_ENDPOINT)
127                    .expect("'send_payment' contains no invalid characters for a URL")
128                    .as_str(),
129            )
130            .json(&SendPaymentPayload {
131                federation_id,
132                outpoint,
133                contract,
134                invoice,
135                auth,
136            })
137            .send()
138            .await
139            .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
140            .json::<Result<[u8; 32], Signature>>()
141            .await
142            .map_err(|e| GatewayConnectionError::Request(e.to_string()))
143    }
144}
145
146#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
147pub struct CreateBolt11InvoicePayload {
148    pub federation_id: FederationId,
149    pub contract: IncomingContract,
150    pub amount: Amount,
151    pub description: Bolt11InvoiceDescription,
152    pub expiry_secs: u32,
153}
154
155#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
156pub struct SendPaymentPayload {
157    pub federation_id: FederationId,
158    pub outpoint: OutPoint,
159    pub contract: OutgoingContract,
160    pub invoice: LightningInvoice,
161    pub auth: Signature,
162}
163
164#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
165pub struct RoutingInfo {
166    /// The public key of the gateways lightning node. Since this key signs the
167    /// gateways invoices the senders client uses it to differentiate between a
168    /// direct swap between fedimints and a lightning swap.
169    pub lightning_public_key: PublicKey,
170    /// The public key of the gateways client module. This key is used to claim
171    /// or cancel outgoing contracts and refund incoming contracts.
172    pub module_public_key: PublicKey,
173    /// This is the fee the gateway charges for an outgoing payment. The senders
174    /// client will use this fee in case of a direct swap.
175    pub send_fee_minimum: PaymentFee,
176    /// This is the default total fee the gateway recommends for an outgoing
177    /// payment in case of a lightning swap. It accounts for the additional fee
178    /// required to reliably route this payment over lightning.
179    pub send_fee_default: PaymentFee,
180    /// This is the minimum expiration delta in block the gateway requires for
181    /// an outgoing payment. The senders client will use this expiration delta
182    /// in case of a direct swap.
183    pub expiration_delta_minimum: u64,
184    /// This is the default total expiration the gateway recommends for an
185    /// outgoing payment in case of a lightning swap. It accounts for the
186    /// additional expiration delta required to successfully route this payment
187    /// over lightning.
188    pub expiration_delta_default: u64,
189    /// This is the fee the gateway charges for an incoming payment.
190    pub receive_fee: PaymentFee,
191}
192
193impl RoutingInfo {
194    pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
195        if invoice.recover_payee_pub_key() == self.lightning_public_key {
196            (self.send_fee_minimum, self.expiration_delta_minimum)
197        } else {
198            (self.send_fee_default, self.expiration_delta_default)
199        }
200    }
201}
202
203#[derive(
204    Debug,
205    Clone,
206    Eq,
207    PartialEq,
208    PartialOrd,
209    Hash,
210    Serialize,
211    Deserialize,
212    Encodable,
213    Decodable,
214    Copy,
215)]
216pub struct PaymentFee {
217    pub base: Amount,
218    pub parts_per_million: u64,
219}
220
221impl PaymentFee {
222    /// This is the maximum send fee of one and a half percent plus one hundred
223    /// satoshis a correct gateway may recommend as a default. It accounts for
224    /// the fee required to reliably route this payment over lightning.
225    pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
226        base: Amount::from_sats(100),
227        parts_per_million: 15_000,
228    };
229
230    /// This is the fee the gateway uses to cover transaction fees with the
231    /// federation.
232    pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
233        base: Amount::from_sats(2),
234        parts_per_million: 3000,
235    };
236
237    /// This is the maximum receive fee of half of one percent plus fifty
238    /// satoshis a correct gateway may recommend as a default.
239    pub const RECEIVE_FEE_LIMIT: PaymentFee = PaymentFee {
240        base: Amount::from_sats(50),
241        parts_per_million: 5_000,
242    };
243
244    pub fn add_to(&self, msats: u64) -> Amount {
245        Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
246    }
247
248    pub fn subtract_from(&self, msats: u64) -> Amount {
249        Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
250    }
251
252    fn absolute_fee(&self, msats: u64) -> u64 {
253        msats
254            .saturating_mul(self.parts_per_million)
255            .saturating_div(1_000_000)
256            .checked_add(self.base.msats)
257            .expect("The division creates sufficient headroom to add the base fee")
258    }
259}
260
261impl Add for PaymentFee {
262    type Output = PaymentFee;
263    fn add(self, rhs: Self) -> Self::Output {
264        PaymentFee {
265            base: self.base + rhs.base,
266            parts_per_million: self.parts_per_million + rhs.parts_per_million,
267        }
268    }
269}
270
271impl From<RoutingFees> for PaymentFee {
272    fn from(value: RoutingFees) -> Self {
273        PaymentFee {
274            base: Amount::from_msats(u64::from(value.base_msat)),
275            parts_per_million: u64::from(value.proportional_millionths),
276        }
277    }
278}
279
280impl From<PaymentFee> for RoutingFees {
281    fn from(value: PaymentFee) -> Self {
282        RoutingFees {
283            base_msat: u32::try_from(value.base.msats).expect("base msat was truncated from u64"),
284            proportional_millionths: u32::try_from(value.parts_per_million)
285                .expect("ppm was truncated from u64"),
286        }
287    }
288}
289
290impl std::fmt::Display for PaymentFee {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        write!(f, "{},{}", self.base, self.parts_per_million)
293    }
294}
295
296impl FromStr for PaymentFee {
297    type Err = anyhow::Error;
298
299    fn from_str(s: &str) -> Result<Self, Self::Err> {
300        let mut parts = s.split(',');
301        let base_str = parts
302            .next()
303            .ok_or(anyhow::anyhow!("Failed to parse base fee"))?;
304        let ppm_str = parts.next().ok_or(anyhow::anyhow!("Failed to parse ppm"))?;
305
306        // Ensure no extra parts
307        if parts.next().is_some() {
308            return Err(anyhow::anyhow!(
309                "Failed to parse fees. Expected format <base>,<ppm>"
310            ));
311        }
312
313        let base = Amount::from_str(base_str)?;
314        let parts_per_million = ppm_str.parse::<u64>()?;
315
316        Ok(PaymentFee {
317            base,
318            parts_per_million,
319        })
320    }
321}