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