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