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