fedimint_lnv2_common/
gateway_api.rs

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