1use std::num::ParseIntError;
2use std::str::FromStr;
3
4use bitcoin::secp256k1::PublicKey;
5use bitcoin::secp256k1::schnorr::Signature;
6use fedimint_connectors::error::ServerError;
7use fedimint_core::config::FederationId;
8use fedimint_core::encoding::{Decodable, Encodable};
9use fedimint_core::util::SafeUrl;
10use fedimint_core::{Amount, OutPoint, ParseAmountError, apply, async_trait_maybe_send};
11use fedimint_ln_common::client::GatewayApi;
12use lightning_invoice::{Bolt11Invoice, RoutingFees};
13use reqwest::Method;
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17use crate::contracts::{IncomingContract, OutgoingContract};
18use crate::endpoint_constants::{
19 CREATE_BOLT11_INVOICE_ENDPOINT, ROUTING_INFO_ENDPOINT, SEND_PAYMENT_ENDPOINT,
20};
21use crate::{Bolt11InvoiceDescription, LightningInvoice};
22
23#[apply(async_trait_maybe_send!)]
24pub trait GatewayConnection: std::fmt::Debug {
25 async fn routing_info(
26 &self,
27 gateway_api: SafeUrl,
28 federation_id: &FederationId,
29 ) -> Result<Option<RoutingInfo>, ServerError>;
30
31 async fn bolt11_invoice(
32 &self,
33 gateway_api: SafeUrl,
34 federation_id: FederationId,
35 contract: IncomingContract,
36 amount: Amount,
37 description: Bolt11InvoiceDescription,
38 expiry_secs: u32,
39 ) -> Result<Bolt11Invoice, ServerError>;
40
41 async fn send_payment(
42 &self,
43 gateway_api: SafeUrl,
44 federation_id: FederationId,
45 outpoint: OutPoint,
46 contract: OutgoingContract,
47 invoice: LightningInvoice,
48 auth: Signature,
49 ) -> Result<Result<[u8; 32], Signature>, ServerError>;
50}
51
52#[derive(Debug, Clone)]
53pub struct RealGatewayConnection {
54 pub api: GatewayApi,
55}
56
57#[apply(async_trait_maybe_send!)]
58impl GatewayConnection for RealGatewayConnection {
59 async fn routing_info(
60 &self,
61 gateway_api: SafeUrl,
62 federation_id: &FederationId,
63 ) -> Result<Option<RoutingInfo>, ServerError> {
64 self.api
65 .request(
66 &gateway_api,
67 Method::POST,
68 ROUTING_INFO_ENDPOINT,
69 Some(federation_id),
70 )
71 .await
72 }
73
74 async fn bolt11_invoice(
75 &self,
76 gateway_api: SafeUrl,
77 federation_id: FederationId,
78 contract: IncomingContract,
79 amount: Amount,
80 description: Bolt11InvoiceDescription,
81 expiry_secs: u32,
82 ) -> Result<Bolt11Invoice, ServerError> {
83 self.api
84 .request(
85 &gateway_api,
86 Method::POST,
87 CREATE_BOLT11_INVOICE_ENDPOINT,
88 Some(CreateBolt11InvoicePayload {
89 federation_id,
90 contract,
91 amount,
92 description,
93 expiry_secs,
94 }),
95 )
96 .await
97 }
98
99 async fn send_payment(
100 &self,
101 gateway_api: SafeUrl,
102 federation_id: FederationId,
103 outpoint: OutPoint,
104 contract: OutgoingContract,
105 invoice: LightningInvoice,
106 auth: Signature,
107 ) -> Result<Result<[u8; 32], Signature>, ServerError> {
108 self.api
109 .request(
110 &gateway_api,
111 Method::POST,
112 SEND_PAYMENT_ENDPOINT,
113 Some(SendPaymentPayload {
114 federation_id,
115 outpoint,
116 contract,
117 invoice,
118 auth,
119 }),
120 )
121 .await
122 }
123}
124
125pub const MAX_INVOICE_EXPIRY_SECS: u32 = 60 * 60 * 24;
130
131#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
132pub struct CreateBolt11InvoicePayload {
133 pub federation_id: FederationId,
134 pub contract: IncomingContract,
135 pub amount: Amount,
136 pub description: Bolt11InvoiceDescription,
137 pub expiry_secs: u32,
138}
139
140#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
141pub struct SendPaymentPayload {
142 pub federation_id: FederationId,
143 pub outpoint: OutPoint,
144 pub contract: OutgoingContract,
145 pub invoice: LightningInvoice,
146 pub auth: Signature,
147}
148
149#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
150pub struct RoutingInfo {
151 pub lightning_public_key: PublicKey,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub lightning_alias: Option<String>,
161 pub module_public_key: PublicKey,
164 pub send_fee_minimum: PaymentFee,
167 pub send_fee_default: PaymentFee,
171 pub expiration_delta_minimum: u64,
175 pub expiration_delta_default: u64,
180 pub receive_fee: PaymentFee,
182 #[serde(default = "default_receive_enabled")]
190 pub receive_enabled: bool,
191}
192
193const fn default_receive_enabled() -> bool {
194 true
195}
196
197impl RoutingInfo {
198 pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
199 if invoice.recover_payee_pub_key() == self.lightning_public_key {
200 (self.send_fee_minimum, self.expiration_delta_minimum)
201 } else {
202 (self.send_fee_default, self.expiration_delta_default)
203 }
204 }
205}
206
207#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable, Copy)]
208pub struct PaymentFee {
209 pub base: Amount,
210 pub parts_per_million: u64,
211}
212
213impl PaymentFee {
214 pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
218 base: Amount::from_sats(100),
219 parts_per_million: 15_000,
220 };
221
222 pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
225 base: Amount::from_sats(2),
226 parts_per_million: 3000,
227 };
228
229 pub const RECEIVE_FEE_LIMIT: PaymentFee = PaymentFee {
232 base: Amount::from_sats(50),
233 parts_per_million: 5_000,
234 };
235
236 pub fn is_within(&self, limit: &PaymentFee) -> bool {
244 self.base <= limit.base && self.parts_per_million <= limit.parts_per_million
245 }
246
247 pub fn checked_add(self, rhs: Self) -> Option<PaymentFee> {
253 Some(PaymentFee {
254 base: self.base.checked_add(rhs.base)?,
255 parts_per_million: self.parts_per_million.checked_add(rhs.parts_per_million)?,
256 })
257 }
258
259 pub fn add_to(&self, msats: u64) -> Amount {
260 Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
261 }
262
263 pub fn subtract_from(&self, msats: u64) -> Amount {
264 Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
265 }
266
267 pub fn fee(&self, msats: u64) -> Amount {
268 Amount::from_msats(self.absolute_fee(msats))
269 }
270
271 fn absolute_fee(&self, msats: u64) -> u64 {
272 msats
276 .saturating_mul(self.parts_per_million)
277 .saturating_div(1_000_000)
278 .saturating_add(self.base.msats)
279 }
280}
281
282#[derive(Debug, Error)]
285#[error("Payment fee {0} exceeds the range of RoutingFees")]
286pub struct FeeOutOfRangeError(PaymentFee);
287
288#[derive(Debug, Error)]
296#[non_exhaustive]
297pub enum ParsePaymentFeeError {
298 #[error("Expected the format <base>,<ppm>")]
300 Format,
301
302 #[error("The base fee is not an amount: {0}")]
304 Base(#[from] ParseAmountError),
305
306 #[error("The relative fee is not a number of parts per million: {0}")]
308 PartsPerMillion(#[from] ParseIntError),
309}
310
311impl From<RoutingFees> for PaymentFee {
312 fn from(value: RoutingFees) -> Self {
313 PaymentFee {
314 base: Amount::from_msats(u64::from(value.base_msat)),
315 parts_per_million: u64::from(value.proportional_millionths),
316 }
317 }
318}
319
320impl TryFrom<PaymentFee> for RoutingFees {
321 type Error = FeeOutOfRangeError;
322
323 fn try_from(value: PaymentFee) -> Result<Self, Self::Error> {
324 Ok(RoutingFees {
325 base_msat: u32::try_from(value.base.msats).map_err(|_| FeeOutOfRangeError(value))?,
326 proportional_millionths: u32::try_from(value.parts_per_million)
327 .map_err(|_| FeeOutOfRangeError(value))?,
328 })
329 }
330}
331
332impl std::fmt::Display for PaymentFee {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 write!(f, "{},{}", self.base, self.parts_per_million)
335 }
336}
337
338impl FromStr for PaymentFee {
339 type Err = ParsePaymentFeeError;
340
341 fn from_str(s: &str) -> Result<Self, Self::Err> {
342 let (base_str, ppm_str) = s.split_once(',').ok_or(ParsePaymentFeeError::Format)?;
346
347 if ppm_str.contains(',') {
348 return Err(ParsePaymentFeeError::Format);
349 }
350
351 Ok(PaymentFee {
352 base: Amount::from_str(base_str)?,
353 parts_per_million: ppm_str.parse::<u64>()?,
354 })
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use fedimint_core::Amount;
361 use lightning_invoice::RoutingFees;
362
363 use super::{ParsePaymentFeeError, PaymentFee};
364
365 #[test]
368 fn is_within_enforces_both_components() {
369 let over_ppm = PaymentFee {
371 base: Amount::from_sats(0),
372 parts_per_million: 1_000_000,
373 };
374 assert!(!over_ppm.is_within(&PaymentFee::SEND_FEE_LIMIT));
375
376 let over_ppm = PaymentFee {
377 base: Amount::from_sats(0),
378 parts_per_million: 5_000_000,
379 };
380 assert!(!over_ppm.is_within(&PaymentFee::SEND_FEE_LIMIT));
381
382 let over_ppm = PaymentFee {
384 base: Amount::from_sats(0),
385 parts_per_million: 500_000,
386 };
387 assert!(!over_ppm.is_within(&PaymentFee::RECEIVE_FEE_LIMIT));
388
389 let over_base = PaymentFee {
391 base: Amount::from_sats(101),
392 parts_per_million: 0,
393 };
394 assert!(!over_base.is_within(&PaymentFee::SEND_FEE_LIMIT));
395
396 assert!(PaymentFee::SEND_FEE_LIMIT.is_within(&PaymentFee::SEND_FEE_LIMIT));
398
399 let ok = PaymentFee {
401 base: Amount::from_sats(50),
402 parts_per_million: 10_000,
403 };
404 assert!(ok.is_within(&PaymentFee::SEND_FEE_LIMIT));
405 }
406
407 #[test]
410 fn checked_add_reports_overflow_instead_of_panicking() {
411 let max_base = PaymentFee {
412 base: Amount::from_msats(u64::MAX),
413 parts_per_million: 0,
414 };
415 let one_msat = PaymentFee {
416 base: Amount::from_msats(1),
417 parts_per_million: 0,
418 };
419 assert!(max_base.checked_add(one_msat).is_none());
420
421 let max_ppm = PaymentFee {
422 base: Amount::ZERO,
423 parts_per_million: u64::MAX,
424 };
425 let one_ppm = PaymentFee {
426 base: Amount::ZERO,
427 parts_per_million: 1,
428 };
429 assert!(max_ppm.checked_add(one_ppm).is_none());
430
431 assert_eq!(
432 PaymentFee::TRANSACTION_FEE_DEFAULT
433 .checked_add(PaymentFee::TRANSACTION_FEE_DEFAULT)
434 .expect("Two default fees fit"),
435 PaymentFee {
436 base: Amount::from_sats(4),
437 parts_per_million: 6_000,
438 }
439 );
440 }
441
442 #[test]
447 fn routing_fees_conversion_rejects_out_of_range_fees() {
448 let over_base = PaymentFee {
449 base: Amount::from_msats(u64::from(u32::MAX) + 1),
450 parts_per_million: 0,
451 };
452 assert!(RoutingFees::try_from(over_base).is_err());
453
454 let over_ppm = PaymentFee {
455 base: Amount::ZERO,
456 parts_per_million: u64::from(u32::MAX) + 1,
457 };
458 assert!(RoutingFees::try_from(over_ppm).is_err());
459
460 let fees = RoutingFees::try_from(PaymentFee::SEND_FEE_LIMIT)
461 .expect("The send fee limit is within range");
462 assert_eq!(fees.base_msat, 100_000);
463 assert_eq!(fees.proportional_millionths, 15_000);
464 }
465
466 #[test]
470 fn absolute_fee_saturates_instead_of_panicking() {
471 let huge = PaymentFee {
472 base: Amount::from_msats(u64::MAX),
473 parts_per_million: u64::MAX,
474 };
475 assert_eq!(huge.fee(u64::MAX), Amount::from_msats(u64::MAX));
476
477 assert_eq!(
478 PaymentFee::TRANSACTION_FEE_DEFAULT.fee(1_000_000),
479 Amount::from_msats(2_000 + 3_000)
480 );
481 }
482
483 #[test]
487 fn parsing_a_fee_names_the_half_that_failed() {
488 assert!(matches!(
489 "1000".parse::<PaymentFee>(),
490 Err(ParsePaymentFeeError::Format)
491 ));
492 assert!(matches!(
493 "".parse::<PaymentFee>(),
494 Err(ParsePaymentFeeError::Format)
495 ));
496 assert!(matches!(
497 "1,2,3".parse::<PaymentFee>(),
498 Err(ParsePaymentFeeError::Format)
499 ));
500 assert!(matches!(
501 "banana,3000".parse::<PaymentFee>(),
502 Err(ParsePaymentFeeError::Base(_))
503 ));
504 assert!(matches!(
505 "2000,banana".parse::<PaymentFee>(),
506 Err(ParsePaymentFeeError::PartsPerMillion(_))
507 ));
508 }
509
510 #[test]
514 fn a_fee_round_trips_through_its_text_form() {
515 let fee = PaymentFee::TRANSACTION_FEE_DEFAULT;
516
517 assert_eq!(
518 fee.to_string()
519 .parse::<PaymentFee>()
520 .expect("The rendered form parses back"),
521 fee
522 );
523 }
524}