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
24const INITIAL_INDEX: u64 = 1;
28
29#[derive(Debug)]
31pub struct FederationManager {
32 clients: BTreeMap<FederationId, Spanned<fedimint_client::ClientHandleArc>>,
35
36 index_to_federation: BTreeMap<u64, FederationId>,
40
41 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 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 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 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 .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 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 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}