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        let endpoint = gateway_api.join(ROUTING_INFO_ENDPOINT).unwrap();
68
69        reqwest::Client::new()
70            .post(endpoint.as_str())
71            .json(federation_id)
72            .send()
73            .await
74            .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
75            .json::<Option<RoutingInfo>>()
76            .await
77            .map_err(|e| GatewayConnectionError::Request(e.to_string()))
78    }
79
80    async fn bolt11_invoice(
81        &self,
82        gateway_api: SafeUrl,
83        federation_id: FederationId,
84        contract: IncomingContract,
85        amount: Amount,
86        description: Bolt11InvoiceDescription,
87        expiry_secs: u32,
88    ) -> Result<Bolt11Invoice, GatewayConnectionError> {
89        let endpoint = gateway_api.join(CREATE_BOLT11_INVOICE_ENDPOINT).unwrap();
90
91        reqwest::Client::new()
92            .post(endpoint.as_str())
93            .json(&CreateBolt11InvoicePayload {
94                federation_id,
95                contract,
96                amount,
97                description,
98                expiry_secs,
99            })
100            .send()
101            .await
102            .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
103            .json::<Bolt11Invoice>()
104            .await
105            .map_err(|e| GatewayConnectionError::Request(e.to_string()))
106    }
107
108    async fn send_payment(
109        &self,
110        gateway_api: SafeUrl,
111        federation_id: FederationId,
112        outpoint: OutPoint,
113        contract: OutgoingContract,
114        invoice: LightningInvoice,
115        auth: Signature,
116    ) -> Result<Result<[u8; 32], Signature>, GatewayConnectionError> {
117        let endpoint = gateway_api.join(SEND_PAYMENT_ENDPOINT).unwrap();
118
119        reqwest::Client::new()
120            .post(endpoint.as_str())
121            .json(&SendPaymentPayload {
122                federation_id,
123                outpoint,
124                contract,
125                invoice,
126                auth,
127            })
128            .send()
129            .await
130            .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
131            .json::<Result<[u8; 32], Signature>>()
132            .await
133            .map_err(|e| GatewayConnectionError::Request(e.to_string()))
134    }
135}
136
137#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
138pub struct CreateBolt11InvoicePayload {
139    pub federation_id: FederationId,
140    pub contract: IncomingContract,
141    pub amount: Amount,
142    pub description: Bolt11InvoiceDescription,
143    pub expiry_secs: u32,
144}
145
146#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
147pub struct SendPaymentPayload {
148    pub federation_id: FederationId,
149    pub outpoint: OutPoint,
150    pub contract: OutgoingContract,
151    pub invoice: LightningInvoice,
152    pub auth: Signature,
153}
154
155#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
156pub struct RoutingInfo {
157    /// The public key of the gateways lightning node. Since this key signs the
158    /// gateways invoices the senders client uses it to differentiate between a
159    /// direct swap between fedimints and a lightning swap.
160    pub lightning_public_key: PublicKey,
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}
183
184impl RoutingInfo {
185    pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
186        if invoice.recover_payee_pub_key() == self.lightning_public_key {
187            (self.send_fee_minimum, self.expiration_delta_minimum)
188        } else {
189            (self.send_fee_default, self.expiration_delta_default)
190        }
191    }
192}
193
194#[derive(
195    Debug,
196    Clone,
197    Eq,
198    PartialEq,
199    PartialOrd,
200    Hash,
201    Serialize,
202    Deserialize,
203    Encodable,
204    Decodable,
205    Copy,
206)]
207pub struct PaymentFee {
208    pub base: Amount,
209    pub parts_per_million: u64,
210}
211
212impl PaymentFee {
213    /// This is the maximum send fee of one and a half percent plus one hundred
214    /// satoshis a correct gateway may recommend as a default. It accounts for
215    /// the fee required to reliably route this payment over lightning.
216    pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
217        base: Amount::from_sats(100),
218        parts_per_million: 15_000,
219    };
220
221    /// This is the fee the gateway uses to cover transaction fees with the
222    /// federation.
223    pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
224        base: Amount::from_sats(2),
225        parts_per_million: 3000,
226    };
227
228    /// This is the maximum receive fee of half of one percent plus fifty
229    /// satoshis a correct gateway may recommend as a default.
230    pub const RECEIVE_FEE_LIMIT: PaymentFee = PaymentFee {
231        base: Amount::from_sats(50),
232        parts_per_million: 5_000,
233    };
234
235    pub fn add_to(&self, msats: u64) -> Amount {
236        Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
237    }
238
239    pub fn subtract_from(&self, msats: u64) -> Amount {
240        Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
241    }
242
243    fn absolute_fee(&self, msats: u64) -> u64 {
244        msats
245            .saturating_mul(self.parts_per_million)
246            .saturating_div(1_000_000)
247            .checked_add(self.base.msats)
248            .expect("The division creates sufficient headroom to add the base fee")
249    }
250}
251
252impl Add for PaymentFee {
253    type Output = PaymentFee;
254    fn add(self, rhs: Self) -> Self::Output {
255        PaymentFee {
256            base: self.base + rhs.base,
257            parts_per_million: self.parts_per_million + rhs.parts_per_million,
258        }
259    }
260}
261
262impl From<RoutingFees> for PaymentFee {
263    fn from(value: RoutingFees) -> Self {
264        PaymentFee {
265            base: Amount::from_msats(u64::from(value.base_msat)),
266            parts_per_million: u64::from(value.proportional_millionths),
267        }
268    }
269}
270
271impl From<PaymentFee> for RoutingFees {
272    fn from(value: PaymentFee) -> Self {
273        RoutingFees {
274            base_msat: u32::try_from(value.base.msats).expect("base msat was truncated from u64"),
275            proportional_millionths: u32::try_from(value.parts_per_million)
276                .expect("ppm was truncated from u64"),
277        }
278    }
279}
280
281impl std::fmt::Display for PaymentFee {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        write!(f, "{},{}", self.base, self.parts_per_million)
284    }
285}
286
287impl FromStr for PaymentFee {
288    type Err = anyhow::Error;
289
290    fn from_str(s: &str) -> Result<Self, Self::Err> {
291        let mut parts = s.split(',');
292        let base_str = parts
293            .next()
294            .ok_or(anyhow::anyhow!("Failed to parse base fee"))?;
295        let ppm_str = parts.next().ok_or(anyhow::anyhow!("Failed to parse ppm"))?;
296
297        // Ensure no extra parts
298        if parts.next().is_some() {
299            return Err(anyhow::anyhow!(
300                "Failed to parse fees. Expected format <base>,<ppm>"
301            ));
302        }
303
304        let base = Amount::from_str(base_str)?;
305        let parts_per_million = ppm_str.parse::<u64>()?;
306
307        Ok(PaymentFee {
308            base,
309            parts_per_million,
310        })
311    }
312}