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