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 let endpoint = gateway_api.join(ROUTING_INFO_ENDPOINT).unwrap();
68
69 reqwest::Client::new()
70 .post(endpoint.as_str())
71 .json(federation_id)
72 .send()
73 .await
74 .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
75 .json::<Option<RoutingInfo>>()
76 .await
77 .map_err(|e| GatewayConnectionError::Request(e.to_string()))
78 }
79
80 async fn bolt11_invoice(
81 &self,
82 gateway_api: SafeUrl,
83 federation_id: FederationId,
84 contract: IncomingContract,
85 amount: Amount,
86 description: Bolt11InvoiceDescription,
87 expiry_secs: u32,
88 ) -> Result<Bolt11Invoice, GatewayConnectionError> {
89 let endpoint = gateway_api.join(CREATE_BOLT11_INVOICE_ENDPOINT).unwrap();
90
91 reqwest::Client::new()
92 .post(endpoint.as_str())
93 .json(&CreateBolt11InvoicePayload {
94 federation_id,
95 contract,
96 amount,
97 description,
98 expiry_secs,
99 })
100 .send()
101 .await
102 .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
103 .json::<Bolt11Invoice>()
104 .await
105 .map_err(|e| GatewayConnectionError::Request(e.to_string()))
106 }
107
108 async fn send_payment(
109 &self,
110 gateway_api: SafeUrl,
111 federation_id: FederationId,
112 outpoint: OutPoint,
113 contract: OutgoingContract,
114 invoice: LightningInvoice,
115 auth: Signature,
116 ) -> Result<Result<[u8; 32], Signature>, GatewayConnectionError> {
117 let endpoint = gateway_api.join(SEND_PAYMENT_ENDPOINT).unwrap();
118
119 reqwest::Client::new()
120 .post(endpoint.as_str())
121 .json(&SendPaymentPayload {
122 federation_id,
123 outpoint,
124 contract,
125 invoice,
126 auth,
127 })
128 .send()
129 .await
130 .map_err(|e| GatewayConnectionError::Unreachable(e.to_string()))?
131 .json::<Result<[u8; 32], Signature>>()
132 .await
133 .map_err(|e| GatewayConnectionError::Request(e.to_string()))
134 }
135}
136
137#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
138pub struct CreateBolt11InvoicePayload {
139 pub federation_id: FederationId,
140 pub contract: IncomingContract,
141 pub amount: Amount,
142 pub description: Bolt11InvoiceDescription,
143 pub expiry_secs: u32,
144}
145
146#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
147pub struct SendPaymentPayload {
148 pub federation_id: FederationId,
149 pub outpoint: OutPoint,
150 pub contract: OutgoingContract,
151 pub invoice: LightningInvoice,
152 pub auth: Signature,
153}
154
155#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
156pub struct RoutingInfo {
157 pub lightning_public_key: PublicKey,
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}
183
184impl RoutingInfo {
185 pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) {
186 if invoice.recover_payee_pub_key() == self.lightning_public_key {
187 (self.send_fee_minimum, self.expiration_delta_minimum)
188 } else {
189 (self.send_fee_default, self.expiration_delta_default)
190 }
191 }
192}
193
194#[derive(
195 Debug,
196 Clone,
197 Eq,
198 PartialEq,
199 PartialOrd,
200 Hash,
201 Serialize,
202 Deserialize,
203 Encodable,
204 Decodable,
205 Copy,
206)]
207pub struct PaymentFee {
208 pub base: Amount,
209 pub parts_per_million: u64,
210}
211
212impl PaymentFee {
213 pub const SEND_FEE_LIMIT: PaymentFee = PaymentFee {
217 base: Amount::from_sats(100),
218 parts_per_million: 15_000,
219 };
220
221 pub const TRANSACTION_FEE_DEFAULT: PaymentFee = PaymentFee {
224 base: Amount::from_sats(2),
225 parts_per_million: 3000,
226 };
227
228 pub const RECEIVE_FEE_LIMIT: PaymentFee = PaymentFee {
231 base: Amount::from_sats(50),
232 parts_per_million: 5_000,
233 };
234
235 pub fn add_to(&self, msats: u64) -> Amount {
236 Amount::from_msats(msats.saturating_add(self.absolute_fee(msats)))
237 }
238
239 pub fn subtract_from(&self, msats: u64) -> Amount {
240 Amount::from_msats(msats.saturating_sub(self.absolute_fee(msats)))
241 }
242
243 fn absolute_fee(&self, msats: u64) -> u64 {
244 msats
245 .saturating_mul(self.parts_per_million)
246 .saturating_div(1_000_000)
247 .checked_add(self.base.msats)
248 .expect("The division creates sufficient headroom to add the base fee")
249 }
250}
251
252impl Add for PaymentFee {
253 type Output = PaymentFee;
254 fn add(self, rhs: Self) -> Self::Output {
255 PaymentFee {
256 base: self.base + rhs.base,
257 parts_per_million: self.parts_per_million + rhs.parts_per_million,
258 }
259 }
260}
261
262impl From<RoutingFees> for PaymentFee {
263 fn from(value: RoutingFees) -> Self {
264 PaymentFee {
265 base: Amount::from_msats(u64::from(value.base_msat)),
266 parts_per_million: u64::from(value.proportional_millionths),
267 }
268 }
269}
270
271impl From<PaymentFee> for RoutingFees {
272 fn from(value: PaymentFee) -> Self {
273 RoutingFees {
274 base_msat: u32::try_from(value.base.msats).expect("base msat was truncated from u64"),
275 proportional_millionths: u32::try_from(value.parts_per_million)
276 .expect("ppm was truncated from u64"),
277 }
278 }
279}
280
281impl std::fmt::Display for PaymentFee {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 write!(f, "{},{}", self.base, self.parts_per_million)
284 }
285}
286
287impl FromStr for PaymentFee {
288 type Err = anyhow::Error;
289
290 fn from_str(s: &str) -> Result<Self, Self::Err> {
291 let mut parts = s.split(',');
292 let base_str = parts
293 .next()
294 .ok_or(anyhow::anyhow!("Failed to parse base fee"))?;
295 let ppm_str = parts.next().ok_or(anyhow::anyhow!("Failed to parse ppm"))?;
296
297 if parts.next().is_some() {
299 return Err(anyhow::anyhow!(
300 "Failed to parse fees. Expected format <base>,<ppm>"
301 ));
302 }
303
304 let base = Amount::from_str(base_str)?;
305 let parts_per_million = ppm_str.parse::<u64>()?;
306
307 Ok(PaymentFee {
308 base,
309 parts_per_million,
310 })
311 }
312}