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 pub lightning_public_key: PublicKey,
170 pub module_public_key: PublicKey,
173 pub send_fee_minimum: PaymentFee,
176 pub send_fee_default: PaymentFee,
180 pub expiration_delta_minimum: u64,
184 pub expiration_delta_default: u64,
189 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 pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
226 base: Amount::from_sats(100),
227 parts_per_million: 15_000,
228 };
229
230 pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
233 base: Amount::from_sats(2),
234 parts_per_million: 3000,
235 };
236
237 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 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}