1use std::ops::Add;
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, apply, async_trait_maybe_send};
11use fedimint_ln_common::client::GatewayApi;
12use lightning_invoice::{Bolt11Invoice, RoutingFees};
13use reqwest::Method;
14use serde::{Deserialize, Serialize};
15
16use crate::contracts::{IncomingContract, OutgoingContract};
17use crate::endpoint_constants::{
18 CREATE_BOLT11_INVOICE_ENDPOINT, ROUTING_INFO_ENDPOINT, SEND_PAYMENT_ENDPOINT,
19};
20use crate::{Bolt11InvoiceDescription, LightningInvoice};
21
22#[apply(async_trait_maybe_send!)]
23pub trait GatewayConnection: std::fmt::Debug {
24 async fn routing_info(
25 &self,
26 gateway_api: SafeUrl,
27 federation_id: &FederationId,
28 ) -> Result<Option<RoutingInfo>, ServerError>;
29
30 async fn bolt11_invoice(
31 &self,
32 gateway_api: SafeUrl,
33 federation_id: FederationId,
34 contract: IncomingContract,
35 amount: Amount,
36 description: Bolt11InvoiceDescription,
37 expiry_secs: u32,
38 ) -> Result<Bolt11Invoice, ServerError>;
39
40 async fn send_payment(
41 &self,
42 gateway_api: SafeUrl,
43 federation_id: FederationId,
44 outpoint: OutPoint,
45 contract: OutgoingContract,
46 invoice: LightningInvoice,
47 auth: Signature,
48 ) -> Result<Result<[u8; 32], Signature>, ServerError>;
49}
50
51#[derive(Debug, Clone)]
52pub struct RealGatewayConnection {
53 pub api: GatewayApi,
54}
55
56#[apply(async_trait_maybe_send!)]
57impl GatewayConnection for RealGatewayConnection {
58 async fn routing_info(
59 &self,
60 gateway_api: SafeUrl,
61 federation_id: &FederationId,
62 ) -> Result<Option<RoutingInfo>, ServerError> {
63 self.api
64 .request(
65 &gateway_api,
66 Method::POST,
67 ROUTING_INFO_ENDPOINT,
68 Some(federation_id),
69 )
70 .await
71 }
72
73 async fn bolt11_invoice(
74 &self,
75 gateway_api: SafeUrl,
76 federation_id: FederationId,
77 contract: IncomingContract,
78 amount: Amount,
79 description: Bolt11InvoiceDescription,
80 expiry_secs: u32,
81 ) -> Result<Bolt11Invoice, ServerError> {
82 self.api
83 .request(
84 &gateway_api,
85 Method::POST,
86 CREATE_BOLT11_INVOICE_ENDPOINT,
87 Some(CreateBolt11InvoicePayload {
88 federation_id,
89 contract,
90 amount,
91 description,
92 expiry_secs,
93 }),
94 )
95 .await
96 }
97
98 async fn send_payment(
99 &self,
100 gateway_api: SafeUrl,
101 federation_id: FederationId,
102 outpoint: OutPoint,
103 contract: OutgoingContract,
104 invoice: LightningInvoice,
105 auth: Signature,
106 ) -> Result<Result<[u8; 32], Signature>, ServerError> {
107 self.api
108 .request(
109 &gateway_api,
110 Method::POST,
111 SEND_PAYMENT_ENDPOINT,
112 Some(SendPaymentPayload {
113 federation_id,
114 outpoint,
115 contract,
116 invoice,
117 auth,
118 }),
119 )
120 .await
121 }
122}
123
124#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
125pub struct CreateBolt11InvoicePayload {
126 pub federation_id: FederationId,
127 pub contract: IncomingContract,
128 pub amount: Amount,
129 pub description: Bolt11InvoiceDescription,
130 pub expiry_secs: u32,
131}
132
133#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
134pub struct SendPaymentPayload {
135 pub federation_id: FederationId,
136 pub outpoint: OutPoint,
137 pub contract: OutgoingContract,
138 pub invoice: LightningInvoice,
139 pub auth: Signature,
140}
141
142#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
143pub struct RoutingInfo {
144 pub lightning_public_key: PublicKey,
148 pub module_public_key: PublicKey,
151 pub send_fee_minimum: PaymentFee,
154 pub send_fee_default: PaymentFee,
158 pub expiration_delta_minimum: u64,
162 pub expiration_delta_default: u64,
167 pub receive_fee: PaymentFee,
169}
170
171impl RoutingInfo {
172 pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
173 if invoice.recover_payee_pub_key() == self.lightning_public_key {
174 (self.send_fee_minimum, self.expiration_delta_minimum)
175 } else {
176 (self.send_fee_default, self.expiration_delta_default)
177 }
178 }
179}
180
181#[derive(
182 Debug,
183 Clone,
184 Eq,
185 PartialEq,
186 PartialOrd,
187 Hash,
188 Serialize,
189 Deserialize,
190 Encodable,
191 Decodable,
192 Copy,
193)]
194pub struct PaymentFee {
195 pub base: Amount,
196 pub parts_per_million: u64,
197}
198
199impl PaymentFee {
200 pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
204 base: Amount::from_sats(100),
205 parts_per_million: 15_000,
206 };
207
208 pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
211 base: Amount::from_sats(2),
212 parts_per_million: 3000,
213 };
214
215 pub const RECEIVE_FEE_LIMIT: PaymentFee = PaymentFee {
218 base: Amount::from_sats(50),
219 parts_per_million: 5_000,
220 };
221
222 pub fn add_to(&self, msats: u64) -> Amount {
223 Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
224 }
225
226 pub fn subtract_from(&self, msats: u64) -> Amount {
227 Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
228 }
229
230 pub fn fee(&self, msats: u64) -> Amount {
231 Amount::from_msats(self.absolute_fee(msats))
232 }
233
234 fn absolute_fee(&self, msats: u64) -> u64 {
235 msats
236 .saturating_mul(self.parts_per_million)
237 .saturating_div(1_000_000)
238 .checked_add(self.base.msats)
239 .expect("The division creates sufficient headroom to add the base fee")
240 }
241}
242
243impl Add for PaymentFee {
244 type Output = PaymentFee;
245 fn add(self, rhs: Self) -> Self::Output {
246 PaymentFee {
247 base: self.base + rhs.base,
248 parts_per_million: self.parts_per_million + rhs.parts_per_million,
249 }
250 }
251}
252
253impl From<RoutingFees> for PaymentFee {
254 fn from(value: RoutingFees) -> Self {
255 PaymentFee {
256 base: Amount::from_msats(u64::from(value.base_msat)),
257 parts_per_million: u64::from(value.proportional_millionths),
258 }
259 }
260}
261
262impl From<PaymentFee> for RoutingFees {
263 fn from(value: PaymentFee) -> Self {
264 RoutingFees {
265 base_msat: u32::try_from(value.base.msats).expect("base msat was truncated from u64"),
266 proportional_millionths: u32::try_from(value.parts_per_million)
267 .expect("ppm was truncated from u64"),
268 }
269 }
270}
271
272impl std::fmt::Display for PaymentFee {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 write!(f, "{},{}", self.base, self.parts_per_million)
275 }
276}
277
278impl FromStr for PaymentFee {
279 type Err = anyhow::Error;
280
281 fn from_str(s: &str) -> Result<Self, Self::Err> {
282 let mut parts = s.split(',');
283 let base_str = parts
284 .next()
285 .ok_or(anyhow::anyhow!("Failed to parse base fee"))?;
286 let ppm_str = parts.next().ok_or(anyhow::anyhow!("Failed to parse ppm"))?;
287
288 if parts.next().is_some() {
290 return Err(anyhow::anyhow!(
291 "Failed to parse fees. Expected format <base>,<ppm>"
292 ));
293 }
294
295 let base = Amount::from_str(base_str)?;
296 let parts_per_million = ppm_str.parse::<u64>()?;
297
298 Ok(PaymentFee {
299 base,
300 parts_per_million,
301 })
302 }
303}