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