1use std::collections::{BTreeMap, HashMap};
2use std::time::Duration;
3
4use bitcoin::hashes::sha256::{self, Hash as Sha256Hash};
5use fedimint_api_client::api::{
6 FederationApiExt, FederationError, FederationResult, IModuleFederationApi,
7};
8use fedimint_api_client::query::FilterMapThreshold;
9use fedimint_core::module::ApiRequestErased;
10use fedimint_core::secp256k1::PublicKey;
11use fedimint_core::task::{MaybeSend, MaybeSync};
12use fedimint_core::{apply, async_trait_maybe_send, NumPeersExt, PeerId};
13use fedimint_ln_common::contracts::incoming::{IncomingContractAccount, IncomingContractOffer};
14use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
15use fedimint_ln_common::contracts::{
16 ContractId, DecryptedPreimageStatus, FundedContract, Preimage,
17};
18use fedimint_ln_common::federation_endpoint_constants::{
19 ACCOUNT_ENDPOINT, AWAIT_ACCOUNT_ENDPOINT, AWAIT_BLOCK_HEIGHT_ENDPOINT, AWAIT_OFFER_ENDPOINT,
20 AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT, AWAIT_PREIMAGE_DECRYPTION, BLOCK_COUNT_ENDPOINT,
21 GET_DECRYPTED_PREIMAGE_STATUS, LIST_GATEWAYS_ENDPOINT, OFFER_ENDPOINT,
22 REGISTER_GATEWAY_ENDPOINT, REMOVE_GATEWAY_CHALLENGE_ENDPOINT, REMOVE_GATEWAY_ENDPOINT,
23};
24use fedimint_ln_common::{
25 ContractAccount, LightningGateway, LightningGatewayAnnouncement, RemoveGatewayRequest,
26};
27use itertools::Itertools;
28use tracing::{info, warn};
29
30#[apply(async_trait_maybe_send!)]
31pub trait LnFederationApi {
32 async fn fetch_consensus_block_count(&self) -> FederationResult<Option<u64>>;
33
34 async fn fetch_contract(
35 &self,
36 contract: ContractId,
37 ) -> FederationResult<Option<ContractAccount>>;
38
39 async fn wait_contract(&self, contract: ContractId) -> FederationResult<ContractAccount>;
40
41 async fn wait_block_height(&self, block_height: u64) -> FederationResult<()>;
42
43 async fn wait_outgoing_contract_cancelled(
44 &self,
45 contract: ContractId,
46 ) -> FederationResult<ContractAccount>;
47
48 async fn get_decrypted_preimage_status(
49 &self,
50 contract: ContractId,
51 ) -> FederationResult<(IncomingContractAccount, DecryptedPreimageStatus)>;
52
53 async fn wait_preimage_decrypted(
54 &self,
55 contract: ContractId,
56 ) -> FederationResult<(IncomingContractAccount, Option<Preimage>)>;
57
58 async fn fetch_offer(
59 &self,
60 payment_hash: Sha256Hash,
61 ) -> FederationResult<IncomingContractOffer>;
62
63 async fn fetch_gateways(&self) -> FederationResult<Vec<LightningGatewayAnnouncement>>;
64
65 async fn register_gateway(
66 &self,
67 gateway: &LightningGatewayAnnouncement,
68 ) -> FederationResult<()>;
69
70 async fn get_remove_gateway_challenge(
74 &self,
75 gateway_id: PublicKey,
76 ) -> BTreeMap<PeerId, Option<sha256::Hash>>;
77
78 async fn remove_gateway(&self, remove_gateway_request: RemoveGatewayRequest);
82
83 async fn offer_exists(&self, payment_hash: Sha256Hash) -> FederationResult<bool>;
84
85 async fn get_incoming_contract(
86 &self,
87 id: ContractId,
88 ) -> FederationResult<IncomingContractAccount>;
89
90 async fn get_outgoing_contract(
91 &self,
92 id: ContractId,
93 ) -> FederationResult<OutgoingContractAccount>;
94}
95
96#[apply(async_trait_maybe_send!)]
97impl<T: ?Sized> LnFederationApi for T
98where
99 T: IModuleFederationApi + MaybeSend + MaybeSync + 'static,
100{
101 async fn fetch_consensus_block_count(&self) -> FederationResult<Option<u64>> {
102 self.request_current_consensus(
103 BLOCK_COUNT_ENDPOINT.to_string(),
104 ApiRequestErased::default(),
105 )
106 .await
107 }
108
109 async fn fetch_contract(
110 &self,
111 contract: ContractId,
112 ) -> FederationResult<Option<ContractAccount>> {
113 self.request_current_consensus(
114 ACCOUNT_ENDPOINT.to_string(),
115 ApiRequestErased::new(contract),
116 )
117 .await
118 }
119
120 async fn wait_contract(&self, contract: ContractId) -> FederationResult<ContractAccount> {
121 self.request_current_consensus(
122 AWAIT_ACCOUNT_ENDPOINT.to_string(),
123 ApiRequestErased::new(contract),
124 )
125 .await
126 }
127
128 async fn wait_block_height(&self, block_height: u64) -> FederationResult<()> {
129 self.request_current_consensus(
130 AWAIT_BLOCK_HEIGHT_ENDPOINT.to_string(),
131 ApiRequestErased::new(block_height),
132 )
133 .await
134 }
135
136 async fn wait_outgoing_contract_cancelled(
137 &self,
138 contract: ContractId,
139 ) -> FederationResult<ContractAccount> {
140 self.request_current_consensus(
141 AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT.to_string(),
142 ApiRequestErased::new(contract),
143 )
144 .await
145 }
146
147 async fn get_decrypted_preimage_status(
148 &self,
149 contract: ContractId,
150 ) -> FederationResult<(IncomingContractAccount, DecryptedPreimageStatus)> {
151 self.request_current_consensus(
152 GET_DECRYPTED_PREIMAGE_STATUS.to_string(),
153 ApiRequestErased::new(contract),
154 )
155 .await
156 }
157
158 async fn wait_preimage_decrypted(
159 &self,
160 contract: ContractId,
161 ) -> FederationResult<(IncomingContractAccount, Option<Preimage>)> {
162 self.request_current_consensus(
163 AWAIT_PREIMAGE_DECRYPTION.to_string(),
164 ApiRequestErased::new(contract),
165 )
166 .await
167 }
168
169 async fn fetch_offer(
170 &self,
171 payment_hash: Sha256Hash,
172 ) -> FederationResult<IncomingContractOffer> {
173 self.request_current_consensus(
174 AWAIT_OFFER_ENDPOINT.to_string(),
175 ApiRequestErased::new(payment_hash),
176 )
177 .await
178 }
179
180 async fn fetch_gateways(&self) -> FederationResult<Vec<LightningGatewayAnnouncement>> {
184 let gateway_announcements = self
185 .request_with_strategy(
186 FilterMapThreshold::new(
187 |_, gateways| Ok(gateways),
188 self.all_peers().to_num_peers(),
189 ),
190 LIST_GATEWAYS_ENDPOINT.to_string(),
191 ApiRequestErased::default(),
192 )
193 .await?;
194
195 Ok(filter_duplicate_gateways(&gateway_announcements))
198 }
199
200 async fn register_gateway(
201 &self,
202 gateway: &LightningGatewayAnnouncement,
203 ) -> FederationResult<()> {
204 self.request_current_consensus(
205 REGISTER_GATEWAY_ENDPOINT.to_string(),
206 ApiRequestErased::new(gateway),
207 )
208 .await
209 }
210
211 async fn get_remove_gateway_challenge(
212 &self,
213 gateway_id: PublicKey,
214 ) -> BTreeMap<PeerId, Option<sha256::Hash>> {
215 let mut responses = BTreeMap::new();
216
217 for peer in self.all_peers() {
218 if let Ok(response) = self
219 .request_single_peer_federation::<Option<sha256::Hash>>(
221 Some(Duration::from_secs(1)),
222 REMOVE_GATEWAY_CHALLENGE_ENDPOINT.to_string(),
223 ApiRequestErased::new(gateway_id),
224 *peer,
225 )
226 .await
227 {
228 responses.insert(*peer, response);
229 }
230 }
231
232 responses
233 }
234
235 async fn remove_gateway(&self, remove_gateway_request: RemoveGatewayRequest) {
236 let gateway_id = remove_gateway_request.gateway_id;
237
238 for peer in self.all_peers() {
239 if let Ok(response) = self
240 .request_single_peer_federation::<bool>(
241 Some(Duration::from_secs(1)),
243 REMOVE_GATEWAY_ENDPOINT.to_string(),
244 ApiRequestErased::new(remove_gateway_request.clone()),
245 *peer,
246 )
247 .await
248 {
249 if response {
250 info!("Successfully removed {gateway_id} gateway from peer: {peer}",);
251 } else {
252 warn!("Unable to remove gateway {gateway_id} registration from peer: {peer}");
253 }
254 }
255 }
256 }
257
258 async fn offer_exists(&self, payment_hash: Sha256Hash) -> FederationResult<bool> {
259 Ok(self
260 .request_current_consensus::<Option<IncomingContractOffer>>(
261 OFFER_ENDPOINT.to_string(),
262 ApiRequestErased::new(payment_hash),
263 )
264 .await?
265 .is_some())
266 }
267
268 async fn get_incoming_contract(
269 &self,
270 id: ContractId,
271 ) -> FederationResult<IncomingContractAccount> {
272 let account = self.wait_contract(id).await?;
273 match account.contract {
274 FundedContract::Incoming(c) => Ok(IncomingContractAccount {
275 amount: account.amount,
276 contract: c.contract,
277 }),
278 FundedContract::Outgoing(_) => Err(FederationError::general(
279 AWAIT_ACCOUNT_ENDPOINT,
280 id,
281 anyhow::anyhow!("WrongAccountType"),
282 )),
283 }
284 }
285
286 async fn get_outgoing_contract(
287 &self,
288 id: ContractId,
289 ) -> FederationResult<OutgoingContractAccount> {
290 let account = self.wait_contract(id).await?;
291 match account.contract {
292 FundedContract::Outgoing(c) => Ok(OutgoingContractAccount {
293 amount: account.amount,
294 contract: c,
295 }),
296 FundedContract::Incoming(_) => Err(FederationError::general(
297 AWAIT_ACCOUNT_ENDPOINT,
298 id,
299 anyhow::anyhow!("WrongAccountType"),
300 )),
301 }
302 }
303}
304
305fn filter_duplicate_gateways(
310 gateways: &BTreeMap<PeerId, Vec<LightningGatewayAnnouncement>>,
311) -> Vec<LightningGatewayAnnouncement> {
312 let gateways_by_gateway_id = gateways
313 .values()
314 .flatten()
315 .cloned()
316 .map(|announcement| (announcement.info.gateway_id, announcement))
317 .into_group_map();
318
319 gateways_by_gateway_id
326 .into_values()
327 .flat_map(|announcements| {
328 let mut gateways: HashMap<LightningGateway, Duration> = HashMap::new();
329 for announcement in announcements {
330 let ttl = announcement.ttl;
331 let gateway = announcement.info.clone();
332 gateways
334 .entry(gateway)
335 .and_modify(|t| {
336 if ttl > *t {
337 *t = ttl;
338 }
339 })
340 .or_insert(ttl);
341 }
342
343 gateways
344 .into_iter()
345 .map(|(gateway, ttl)| LightningGatewayAnnouncement {
346 info: gateway,
347 ttl,
348 vetted: false,
349 })
350 })
351 .collect()
352}