Skip to main content

fedimint_gateway_server/
federation_manager.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::SystemTime;
5
6use bitcoin::secp256k1::Keypair;
7use fedimint_client::ClientHandleArc;
8use fedimint_core::config::{FederationId, FederationIdPrefix, JsonClientConfig};
9use fedimint_core::db::{Committable, DatabaseTransaction, NonCommittable};
10use fedimint_core::invite_code::InviteCode;
11use fedimint_core::util::{FmtCompact as _, Spanned};
12use fedimint_core::{PeerId, TieredCounts};
13use fedimint_gateway_common::FederationInfo;
14use fedimint_gateway_server_db::GatewayDbtxNcExt as _;
15use fedimint_gw_client::GatewayClientModule;
16use fedimint_gwv2_client::{GatewayClientModuleV2, GatewayOperationMetaV2};
17use fedimint_logging::LOG_GATEWAY;
18use fedimint_mint_client::MintClientModule;
19use tracing::{info, warn};
20
21use crate::error::{AdminGatewayError, FederationNotConnected};
22use crate::{AdminResult, Registration};
23
24/// The first index that the gateway will assign to a federation.
25/// Note: This starts at 1 because LNv1 uses the `federation_index` as an SCID.
26/// An SCID of 0 is considered invalid by LND's HTLC interceptor.
27const INITIAL_INDEX: u64 = 1;
28
29// TODO: Add support for client lookup by payment hash (for LNv2).
30#[derive(Debug)]
31pub struct FederationManager {
32    /// Map of `FederationId` -> `Client`. Used for efficient retrieval of the
33    /// client while handling incoming HTLCs.
34    clients: BTreeMap<FederationId, Spanned<fedimint_client::ClientHandleArc>>,
35
36    /// Map of federation indices to `FederationId`. Use for efficient retrieval
37    /// of the client while handling incoming HTLCs.
38    /// Can be removed after LNv1 removal.
39    index_to_federation: BTreeMap<u64, FederationId>,
40
41    /// Tracker for federation index assignments. When connecting a new
42    /// federation, this value is incremented and assigned to the federation
43    /// as the `federation_index`
44    next_index: AtomicU64,
45}
46
47impl FederationManager {
48    pub fn new() -> Self {
49        Self {
50            clients: BTreeMap::new(),
51            index_to_federation: BTreeMap::new(),
52            next_index: AtomicU64::new(INITIAL_INDEX),
53        }
54    }
55
56    pub fn add_client(&mut self, index: u64, client: Spanned<fedimint_client::ClientHandleArc>) {
57        let federation_id = client.borrow().with_sync(|c| c.federation_id());
58        self.clients.insert(federation_id, client);
59        self.index_to_federation.insert(index, federation_id);
60    }
61
62    pub async fn leave_federation(
63        &mut self,
64        federation_id: FederationId,
65        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
66        registrations: Vec<&Registration>,
67    ) -> AdminResult<FederationInfo> {
68        let federation_info = self.federation_info(federation_id, dbtx).await?;
69
70        for registration in registrations {
71            self.unannounce_from_federation(federation_id, registration.keypair)
72                .await;
73        }
74
75        self.remove_client(federation_id).await?;
76
77        Ok(federation_info)
78    }
79
80    async fn remove_client(&mut self, federation_id: FederationId) -> AdminResult<()> {
81        let client = self
82            .clients
83            .remove(&federation_id)
84            .ok_or(FederationNotConnected {
85                federation_id_prefix: federation_id.to_prefix(),
86            })?
87            .into_value();
88
89        self.index_to_federation
90            .retain(|_, fid| *fid != federation_id);
91
92        match Arc::into_inner(client) {
93            Some(client) => {
94                client.shutdown().await;
95                Ok(())
96            }
97            _ => Err(AdminGatewayError::ClientRemovalError(format!(
98                "Federation client {federation_id} is not unique, failed to shutdown client"
99            ))),
100        }
101    }
102
103    /// Waits for ongoing incoming LNv1 and LNv2 payments to complete before
104    /// returning.
105    pub async fn wait_for_incoming_payments(&self) -> AdminResult<()> {
106        for client in self.clients.values() {
107            let active_operations = client.value().get_active_operations().await;
108            let operation_log = client.value().operation_log();
109            for op_id in active_operations {
110                let log_entry = operation_log.get_operation(op_id).await;
111                if let Some(entry) = log_entry {
112                    match entry.operation_module_kind() {
113                        "lnv2" => {
114                            let Ok(meta) = entry.try_meta::<GatewayOperationMetaV2>() else {
115                                warn!(
116                                    target: LOG_GATEWAY,
117                                    operation_id = %op_id.fmt_short(),
118                                    "Skipping LNv2 operation with invalid metadata while waiting for incoming payments",
119                                );
120                                continue;
121                            };
122                            if !meta.waits_for_completion() {
123                                continue;
124                            }
125                            let lnv2 = client
126                                .value()
127                                .get_first_module::<GatewayClientModuleV2>()
128                                .map_err(anyhow::Error::from)?;
129                            lnv2.await_completion(op_id).await;
130                        }
131                        "ln" => {
132                            let lnv1 = client
133                                .value()
134                                .get_first_module::<GatewayClientModule>()
135                                .map_err(anyhow::Error::from)?;
136                            lnv1.await_completion(op_id).await;
137                        }
138                        _ => {}
139                    }
140                }
141            }
142        }
143
144        info!(target: LOG_GATEWAY, "Finished waiting for incoming payments");
145        Ok(())
146    }
147
148    async fn unannounce_from_federation(
149        &self,
150        federation_id: FederationId,
151        gateway_keypair: Keypair,
152    ) {
153        if let Ok(client) = self
154            .clients
155            .get(&federation_id)
156            .ok_or(FederationNotConnected {
157                federation_id_prefix: federation_id.to_prefix(),
158            })
159            && let Ok(ln) = client.value().get_first_module::<GatewayClientModule>()
160        {
161            ln.remove_from_federation(gateway_keypair).await;
162        }
163    }
164
165    /// Iterates through all of the federations the gateway is registered with
166    /// and requests to remove the registration record.
167    pub async fn unannounce_from_all_federations(&self, gateway_keypair: Keypair) {
168        let removal_futures = self
169            .clients
170            .values()
171            .filter_map(|client| {
172                client
173                    .value()
174                    .get_first_module::<GatewayClientModule>()
175                    .ok()
176                    .map(|lnv1| async move {
177                        lnv1.remove_from_federation(gateway_keypair).await;
178                    })
179            })
180            .collect::<Vec<_>>();
181
182        futures::future::join_all(removal_futures).await;
183    }
184
185    pub fn get_client_for_index(&self, short_channel_id: u64) -> Option<Spanned<ClientHandleArc>> {
186        let federation_id = self.index_to_federation.get(&short_channel_id)?;
187        // TODO(tvolk131): Cloning the client here could cause issues with client
188        // shutdown (see `remove_client` above). Perhaps this function should take a
189        // lambda and pass it into `client.with_sync`.
190        match self.clients.get(federation_id).cloned() {
191            Some(client) => Some(client),
192            _ => {
193                panic!(
194                    "`FederationManager.index_to_federation` is out of sync with `FederationManager.clients`! This is a bug."
195                );
196            }
197        }
198    }
199
200    pub fn get_client_for_federation_id_prefix(
201        &self,
202        federation_id_prefix: FederationIdPrefix,
203    ) -> Option<Spanned<ClientHandleArc>> {
204        self.clients.iter().find_map(|(fid, client)| {
205            if fid.to_prefix() == federation_id_prefix {
206                Some(client.clone())
207            } else {
208                None
209            }
210        })
211    }
212
213    pub fn has_federation(&self, federation_id: FederationId) -> bool {
214        self.clients.contains_key(&federation_id)
215    }
216
217    pub fn client(&self, federation_id: &FederationId) -> Option<&Spanned<ClientHandleArc>> {
218        self.clients.get(federation_id)
219    }
220
221    pub async fn federation_info(
222        &self,
223        federation_id: FederationId,
224        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
225    ) -> std::result::Result<FederationInfo, FederationNotConnected> {
226        self.clients
227            .get(&federation_id)
228            .ok_or(FederationNotConnected {
229                federation_id_prefix: federation_id.to_prefix(),
230            })?
231            .borrow()
232            .with(|client| async move {
233                let balance_msat = client
234                    .get_balance_for_btc()
235                    .await
236                    // If primary module is not available, we're not really connected yet
237                    .map_err(|_err| FederationNotConnected {
238                        federation_id_prefix: federation_id.to_prefix(),
239                    })?;
240
241                let config = dbtx.load_federation_config(federation_id).await.ok_or(
242                    FederationNotConnected {
243                        federation_id_prefix: federation_id.to_prefix(),
244                    },
245                )?;
246                let last_backup_time =
247                    dbtx.load_backup_record(federation_id)
248                        .await
249                        .ok_or(FederationNotConnected {
250                            federation_id_prefix: federation_id.to_prefix(),
251                        })?;
252
253                Ok(FederationInfo {
254                    federation_id,
255                    federation_name: self.federation_name(client).await,
256                    balance_msat,
257                    config,
258                    last_backup_time,
259                })
260            })
261            .await
262    }
263
264    pub async fn federation_name(&self, client: &ClientHandleArc) -> Option<String> {
265        let client_config = client.config().await;
266        let federation_name = client_config.global.federation_name();
267        federation_name.map(String::from)
268    }
269
270    pub async fn federation_info_all_federations(
271        &self,
272        mut dbtx: DatabaseTransaction<'_, NonCommittable>,
273    ) -> Vec<FederationInfo> {
274        let mut federation_infos = Vec::new();
275        for (federation_id, client) in &self.clients {
276            let balance_msat = match client
277                .borrow()
278                .with(|client| client.get_balance_for_btc())
279                .await
280            {
281                Ok(balance_msat) => balance_msat,
282                Err(err) => {
283                    warn!(
284                        target: LOG_GATEWAY,
285                        err = %err.fmt_compact(),
286                        "Skipped Federation due to lack of primary module"
287                    );
288                    continue;
289                }
290            };
291
292            let config = dbtx.load_federation_config(*federation_id).await;
293            let last_backup_time = dbtx
294                .load_backup_record(*federation_id)
295                .await
296                .unwrap_or_default();
297            if let Some(config) = config {
298                federation_infos.push(FederationInfo {
299                    federation_id: *federation_id,
300                    federation_name: self.federation_name(client.value()).await,
301                    balance_msat,
302                    config,
303                    last_backup_time,
304                });
305            }
306        }
307        federation_infos
308    }
309
310    pub async fn get_federation_config(
311        &self,
312        federation_id: FederationId,
313    ) -> AdminResult<JsonClientConfig> {
314        let client = self
315            .clients
316            .get(&federation_id)
317            .ok_or(FederationNotConnected {
318                federation_id_prefix: federation_id.to_prefix(),
319            })?;
320        Ok(client
321            .borrow()
322            .with(|client| client.get_config_json())
323            .await)
324    }
325
326    pub async fn get_all_federation_configs(&self) -> BTreeMap<FederationId, JsonClientConfig> {
327        let mut federations = BTreeMap::new();
328        for (federation_id, client) in &self.clients {
329            federations.insert(
330                *federation_id,
331                client
332                    .borrow()
333                    .with(|client| client.get_config_json())
334                    .await,
335            );
336        }
337        federations
338    }
339
340    pub async fn backup_federation(
341        &self,
342        federation_id: &FederationId,
343        dbtx: &mut DatabaseTransaction<'_, Committable>,
344        now: SystemTime,
345    ) {
346        if let Some(client) = self.client(federation_id) {
347            let metadata: BTreeMap<String, String> = BTreeMap::new();
348            #[allow(deprecated)]
349            if client
350                .value()
351                .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
352                    metadata,
353                ))
354                .await
355                .is_ok()
356            {
357                dbtx.save_federation_backup_record(*federation_id, Some(now))
358                    .await;
359                info!(federation_id = %federation_id, "Successfully backed up federation");
360            }
361        }
362    }
363
364    pub async fn all_invite_codes(
365        &self,
366    ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
367        let mut invite_codes = BTreeMap::new();
368
369        for (federation_id, client) in &self.clients {
370            let config = client.value().config().await;
371            let api_endpoints = &config.global.api_endpoints;
372
373            let mut fed_invite_codes = BTreeMap::new();
374            for (peer_id, peer_url) in api_endpoints {
375                if let Some(code) = client.value().invite_code(*peer_id).await {
376                    fed_invite_codes.insert(*peer_id, (peer_url.name.clone(), code));
377                }
378            }
379
380            invite_codes.insert(*federation_id, fed_invite_codes);
381        }
382
383        invite_codes
384    }
385
386    pub async fn get_note_summary(
387        &self,
388        federation_id: &FederationId,
389    ) -> AdminResult<TieredCounts> {
390        let client = self.client(federation_id).ok_or(FederationNotConnected {
391            federation_id_prefix: federation_id.to_prefix(),
392        })?;
393        let mint = client
394            .value()
395            .get_first_module::<MintClientModule>()
396            .map_err(anyhow::Error::from)?;
397        let mut dbtx = mint.client_ctx.module_db().begin_transaction_nc().await;
398        let counts = mint.get_note_counts_by_denomination(&mut dbtx).await;
399        info!(target: LOG_GATEWAY, ?counts, "Note counts");
400        Ok(counts)
401    }
402
403    // TODO(tvolk131): Set this value in the constructor.
404    pub fn set_next_index(&self, next_index: u64) {
405        self.next_index.store(next_index, Ordering::SeqCst);
406    }
407
408    pub fn pop_next_index(&self) -> AdminResult<u64> {
409        let next_index = self.next_index.fetch_add(1, Ordering::Relaxed);
410
411        // Check for overflow.
412        if next_index == INITIAL_INDEX.wrapping_sub(1) {
413            return Err(AdminGatewayError::GatewayConfigurationError(
414                "Federation Index overflow".to_string(),
415            ));
416        }
417
418        Ok(next_index)
419    }
420}