1use std::cmp::Ordering;
3use std::collections::{BTreeMap, HashMap};
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use anyhow::{Context, Result};
8use async_trait::async_trait;
9use bitcoin::hashes::sha256;
10use fedimint_aead::{encrypt, get_encryption_key, random_salt};
11use fedimint_api_client::api::{
12 GuardianConfigBackup, LegacyFederationStatus, LegacyP2PConnectionStatus, LegacyPeerStatus,
13 StatusResponse,
14};
15use fedimint_core::admin_client::{ServerStatusLegacy, SetupStatus};
16use fedimint_core::backup::{
17 BackupStatistics, ClientBackupKey, ClientBackupKeyPrefix, ClientBackupSnapshot,
18};
19use fedimint_core::config::{ClientConfig, JsonClientConfig, META_FEDERATION_NAME_KEY};
20use fedimint_core::core::backup::{BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, SignedBackupRequest};
21use fedimint_core::core::{DynOutputOutcome, ModuleInstanceId, ModuleKind};
22use fedimint_core::db::{
23 Committable, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
24};
25#[allow(deprecated)]
26use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
27use fedimint_core::endpoint_constants::{
28 API_ANNOUNCEMENTS_ENDPOINT, AUDIT_ENDPOINT, AUTH_ENDPOINT, AWAIT_SESSION_OUTCOME_ENDPOINT,
29 AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT, AWAIT_TRANSACTION_ENDPOINT, BACKUP_ENDPOINT,
30 BACKUP_STATISTICS_ENDPOINT, CLIENT_CONFIG_ENDPOINT, CLIENT_CONFIG_JSON_ENDPOINT,
31 CONSENSUS_ORD_LATENCY_ENDPOINT, FEDERATION_ID_ENDPOINT, FEDIMINTD_VERSION_ENDPOINT,
32 GUARDIAN_CONFIG_BACKUP_ENDPOINT, INVITE_CODE_ENDPOINT, P2P_CONNECTION_STATUS_ENDPOINT,
33 RECOVER_ENDPOINT, SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT, SESSION_COUNT_ENDPOINT,
34 SESSION_STATUS_ENDPOINT, SESSION_STATUS_V2_ENDPOINT, SETUP_STATUS_ENDPOINT, SHUTDOWN_ENDPOINT,
35 SIGN_API_ANNOUNCEMENT_ENDPOINT, STATUS_ENDPOINT, SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
36 SUBMIT_TRANSACTION_ENDPOINT, VERSION_ENDPOINT,
37};
38use fedimint_core::epoch::ConsensusItem;
39use fedimint_core::module::audit::{Audit, AuditSummary};
40use fedimint_core::module::{
41 ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiResult, ApiVersion,
42 SerdeModuleEncoding, SerdeModuleEncodingBase64, SupportedApiVersionsSummary, api_endpoint,
43};
44use fedimint_core::net::api_announcement::{
45 ApiAnnouncement, SignedApiAnnouncement, SignedApiAnnouncementSubmission,
46};
47use fedimint_core::secp256k1::{PublicKey, SECP256K1};
48use fedimint_core::session_outcome::{
49 SessionOutcome, SessionStatus, SessionStatusV2, SignedSessionOutcome,
50};
51use fedimint_core::transaction::{
52 SerdeTransaction, Transaction, TransactionError, TransactionSubmissionOutcome,
53};
54use fedimint_core::util::{FmtCompact, SafeUrl};
55use fedimint_core::{OutPoint, PeerId, TransactionId, secp256k1};
56use fedimint_logging::LOG_NET_API;
57use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
58use fedimint_server_core::dashboard_ui::{IDashboardApi, ServerBitcoinRpcStatus};
59use fedimint_server_core::net::{GuardianAuthToken, check_auth};
60use fedimint_server_core::{DynServerModule, ServerModuleRegistry, ServerModuleRegistryExt};
61use futures::StreamExt;
62use tokio::sync::watch::{self, Receiver, Sender};
63use tracing::{debug, info, warn};
64
65use crate::config::io::{
66 CONSENSUS_CONFIG, ENCRYPTED_EXT, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG, SALT_FILE,
67};
68use crate::config::{ServerConfig, legacy_consensus_config_hash};
69use crate::consensus::db::{AcceptedItemPrefix, AcceptedTransactionKey, SignedSessionOutcomeKey};
70use crate::consensus::engine::get_finished_session_count_static;
71use crate::consensus::transaction::{TxProcessingMode, process_transaction_with_dbtx};
72use crate::metrics::{BACKUP_WRITE_SIZE_BYTES, STORED_BACKUPS_COUNT};
73use crate::net::api::HasApiContext;
74use crate::net::api::announcement::{ApiAnnouncementKey, ApiAnnouncementPrefix};
75use crate::net::p2p::P2PStatusReceivers;
76
77#[derive(Clone)]
78pub struct ConsensusApi {
79 pub cfg: ServerConfig,
81 pub db: Database,
83 pub modules: ServerModuleRegistry,
85 pub client_cfg: ClientConfig,
87 pub force_api_secret: Option<String>,
88 pub submission_sender: async_channel::Sender<ConsensusItem>,
90 pub shutdown_receiver: Receiver<Option<u64>>,
91 pub shutdown_sender: Sender<Option<u64>>,
92 pub ord_latency_receiver: watch::Receiver<Option<Duration>>,
93 pub p2p_status_receivers: P2PStatusReceivers,
94 pub ci_status_receivers: BTreeMap<PeerId, Receiver<Option<u64>>>,
95 pub bitcoin_rpc_connection: ServerBitcoinRpcMonitor,
96 pub supported_api_versions: SupportedApiVersionsSummary,
97 pub code_version_str: String,
98}
99
100impl ConsensusApi {
101 pub fn api_versions_summary(&self) -> &SupportedApiVersionsSummary {
102 &self.supported_api_versions
103 }
104
105 pub fn get_active_api_secret(&self) -> Option<String> {
106 self.force_api_secret.clone()
109 }
110
111 pub async fn submit_transaction(
114 &self,
115 transaction: Transaction,
116 ) -> Result<TransactionId, TransactionError> {
117 let txid = transaction.tx_hash();
118
119 debug!(target: LOG_NET_API, %txid, "Received a submitted transaction");
120
121 let mut dbtx = self.db.begin_transaction_nc().await;
123 if dbtx
125 .get_value(&AcceptedTransactionKey(txid))
126 .await
127 .is_some()
128 {
129 debug!(target: LOG_NET_API, %txid, "Transaction already accepted");
130 return Ok(txid);
131 }
132
133 dbtx.ignore_uncommitted();
135
136 process_transaction_with_dbtx(
137 self.modules.clone(),
138 &mut dbtx,
139 &transaction,
140 self.cfg.consensus.version,
141 TxProcessingMode::Submission,
142 )
143 .await
144 .inspect_err(|err| {
145 debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
146 })?;
147
148 let _ = self
149 .submission_sender
150 .send(ConsensusItem::Transaction(transaction.clone()))
151 .await
152 .inspect_err(|err| {
153 warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
154 });
155
156 Ok(txid)
157 }
158
159 pub async fn await_transaction(
160 &self,
161 txid: TransactionId,
162 ) -> (Vec<ModuleInstanceId>, DatabaseTransaction<'_, Committable>) {
163 self.db
164 .wait_key_check(&AcceptedTransactionKey(txid), std::convert::identity)
165 .await
166 }
167
168 pub async fn await_output_outcome(
169 &self,
170 outpoint: OutPoint,
171 ) -> Result<SerdeModuleEncoding<DynOutputOutcome>> {
172 let (module_ids, mut dbtx) = self.await_transaction(outpoint.txid).await;
173
174 let module_id = module_ids
175 .into_iter()
176 .nth(outpoint.out_idx as usize)
177 .context("Outpoint index out of bounds {outpoint:?}")?;
178
179 #[allow(deprecated)]
180 let outcome = self
181 .modules
182 .get_expect(module_id)
183 .output_status(
184 &mut dbtx.to_ref_with_prefix_module_id(module_id).0.into_nc(),
185 outpoint,
186 module_id,
187 )
188 .await
189 .context("No output outcome for outpoint")?;
190
191 Ok((&outcome).into())
192 }
193
194 pub async fn session_count(&self) -> u64 {
195 get_finished_session_count_static(&mut self.db.begin_transaction_nc().await).await
196 }
197
198 pub async fn await_signed_session_outcome(&self, index: u64) -> SignedSessionOutcome {
199 self.db
200 .wait_key_check(&SignedSessionOutcomeKey(index), std::convert::identity)
201 .await
202 .0
203 }
204
205 pub async fn session_status(&self, session_index: u64) -> SessionStatusV2 {
206 let mut dbtx = self.db.begin_transaction_nc().await;
207
208 match session_index.cmp(&get_finished_session_count_static(&mut dbtx).await) {
209 Ordering::Greater => SessionStatusV2::Initial,
210 Ordering::Equal => SessionStatusV2::Pending(
211 dbtx.find_by_prefix(&AcceptedItemPrefix)
212 .await
213 .map(|entry| entry.1)
214 .collect()
215 .await,
216 ),
217 Ordering::Less => SessionStatusV2::Complete(
218 dbtx.get_value(&SignedSessionOutcomeKey(session_index))
219 .await
220 .expect("There are no gaps in session outcomes"),
221 ),
222 }
223 }
224
225 pub async fn get_federation_status(&self) -> ApiResult<LegacyFederationStatus> {
226 let session_count = self.session_count().await;
227 let scheduled_shutdown = self.shutdown_receiver.borrow().to_owned();
228
229 let status_by_peer = self
230 .p2p_status_receivers
231 .iter()
232 .map(|(peer, p2p_receiver)| {
233 let ci_receiver = self.ci_status_receivers.get(peer).unwrap();
234
235 let consensus_status = LegacyPeerStatus {
236 connection_status: match *p2p_receiver.borrow() {
237 Some(..) => LegacyP2PConnectionStatus::Connected,
238 None => LegacyP2PConnectionStatus::Disconnected,
239 },
240 last_contribution: *ci_receiver.borrow(),
241 flagged: ci_receiver.borrow().unwrap_or(0) + 1 < session_count,
242 };
243
244 (*peer, consensus_status)
245 })
246 .collect::<HashMap<PeerId, LegacyPeerStatus>>();
247
248 let peers_flagged = status_by_peer
249 .values()
250 .filter(|status| status.flagged)
251 .count() as u64;
252
253 let peers_online = status_by_peer
254 .values()
255 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Connected)
256 .count() as u64;
257
258 let peers_offline = status_by_peer
259 .values()
260 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Disconnected)
261 .count() as u64;
262
263 Ok(LegacyFederationStatus {
264 session_count,
265 status_by_peer,
266 peers_online,
267 peers_offline,
268 peers_flagged,
269 scheduled_shutdown,
270 })
271 }
272
273 fn shutdown(&self, index: Option<u64>) {
274 self.shutdown_sender.send_replace(index);
275 }
276
277 async fn get_federation_audit(&self) -> ApiResult<AuditSummary> {
278 let mut dbtx = self.db.begin_transaction_nc().await;
279 dbtx.ignore_uncommitted();
283
284 let mut audit = Audit::default();
285 let mut module_instance_id_to_kind: HashMap<ModuleInstanceId, String> = HashMap::new();
286 for (module_instance_id, kind, module) in self.modules.iter_modules() {
287 module_instance_id_to_kind.insert(module_instance_id, kind.as_str().to_string());
288 module
289 .audit(
290 &mut dbtx.to_ref_with_prefix_module_id(module_instance_id).0,
291 &mut audit,
292 module_instance_id,
293 )
294 .await;
295 }
296 Ok(AuditSummary::from_audit(
297 &audit,
298 &module_instance_id_to_kind,
299 ))
300 }
301
302 fn get_guardian_config_backup(
307 &self,
308 password: &str,
309 _auth: &GuardianAuthToken,
310 ) -> GuardianConfigBackup {
311 let mut tar_archive_builder = tar::Builder::new(Vec::new());
312
313 let mut append = |name: &Path, data: &[u8]| {
314 let mut header = tar::Header::new_gnu();
315 header.set_path(name).expect("Error setting path");
316 header.set_size(data.len() as u64);
317 header.set_mode(0o644);
318 header.set_cksum();
319 tar_archive_builder
320 .append(&header, data)
321 .expect("Error adding data to tar archive");
322 };
323
324 append(
325 &PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
326 &serde_json::to_vec(&self.cfg.local).expect("Error encoding local config"),
327 );
328
329 append(
330 &PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
331 &serde_json::to_vec(&self.cfg.consensus).expect("Error encoding consensus config"),
332 );
333
334 let encryption_salt = random_salt();
340 append(&PathBuf::from(SALT_FILE), encryption_salt.as_bytes());
341
342 let private_config_bytes =
343 serde_json::to_vec(&self.cfg.private).expect("Error encoding private config");
344 let encryption_key = get_encryption_key(password, &encryption_salt)
345 .expect("Generating key from password failed");
346 let private_config_encrypted =
347 hex::encode(encrypt(private_config_bytes, &encryption_key).expect("Encryption failed"));
348 append(
349 &PathBuf::from(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT),
350 private_config_encrypted.as_bytes(),
351 );
352
353 let tar_archive_bytes = tar_archive_builder
354 .into_inner()
355 .expect("Error building tar archive");
356
357 GuardianConfigBackup { tar_archive_bytes }
358 }
359
360 async fn handle_backup_request<'s, 'dbtx, 'a>(
361 &'s self,
362 dbtx: &'dbtx mut DatabaseTransaction<'a>,
363 request: SignedBackupRequest,
364 ) -> Result<(), ApiError> {
365 let request = request
366 .verify_valid(SECP256K1)
367 .map_err(|_| ApiError::bad_request("invalid request".into()))?;
368
369 if request.payload.len() > BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES {
370 return Err(ApiError::bad_request("snapshot too large".into()));
371 }
372 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request");
373 if let Some(prev) = dbtx.get_value(&ClientBackupKey(request.id)).await {
374 if request.timestamp <= prev.timestamp {
375 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request with old timestamp - ignoring");
376 return Err(ApiError::bad_request("timestamp too small".into()));
377 }
378 }
379
380 info!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Storing new client backup");
381 let overwritten = dbtx
382 .insert_entry(
383 &ClientBackupKey(request.id),
384 &ClientBackupSnapshot {
385 timestamp: request.timestamp,
386 data: request.payload.clone(),
387 },
388 )
389 .await
390 .is_some();
391 BACKUP_WRITE_SIZE_BYTES.observe(request.payload.len() as f64);
392 if !overwritten {
393 dbtx.on_commit(|| STORED_BACKUPS_COUNT.inc());
394 }
395
396 Ok(())
397 }
398
399 async fn handle_recover_request(
400 &self,
401 dbtx: &mut DatabaseTransaction<'_>,
402 id: PublicKey,
403 ) -> Option<ClientBackupSnapshot> {
404 dbtx.get_value(&ClientBackupKey(id)).await
405 }
406
407 async fn api_announcements(&self) -> BTreeMap<PeerId, SignedApiAnnouncement> {
410 self.db
411 .begin_transaction_nc()
412 .await
413 .find_by_prefix(&ApiAnnouncementPrefix)
414 .await
415 .map(|(announcement_key, announcement)| (announcement_key.0, announcement))
416 .collect()
417 .await
418 }
419
420 fn fedimintd_version(&self) -> String {
422 self.code_version_str.clone()
423 }
424
425 async fn submit_api_announcement(
428 &self,
429 peer_id: PeerId,
430 announcement: SignedApiAnnouncement,
431 ) -> Result<(), ApiError> {
432 let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
433 return Err(ApiError::bad_request("Peer not in federation".into()));
434 };
435
436 if !announcement.verify(SECP256K1, peer_key) {
437 return Err(ApiError::bad_request("Invalid signature".into()));
438 }
439
440 let mut dbtx = self.db.begin_transaction().await;
441
442 if let Some(existing_announcement) = dbtx.get_value(&ApiAnnouncementKey(peer_id)).await {
443 if existing_announcement.api_announcement == announcement.api_announcement {
447 return Ok(());
448 }
449
450 if existing_announcement.api_announcement.nonce >= announcement.api_announcement.nonce {
453 return Err(ApiError::bad_request(
454 "Outdated or redundant announcement".into(),
455 ));
456 }
457 }
458
459 dbtx.insert_entry(&ApiAnnouncementKey(peer_id), &announcement)
460 .await;
461 dbtx.commit_tx().await;
462 Ok(())
463 }
464
465 async fn sign_api_announcement(&self, new_url: SafeUrl) -> SignedApiAnnouncement {
466 self.db
467 .autocommit(
468 |dbtx, _| {
469 let new_url_inner = new_url.clone();
470 Box::pin(async move {
471 let new_nonce = dbtx
472 .get_value(&ApiAnnouncementKey(self.cfg.local.identity))
473 .await
474 .map_or(0, |a| a.api_announcement.nonce + 1);
475 let announcement = ApiAnnouncement {
476 api_url: new_url_inner,
477 nonce: new_nonce,
478 };
479 let ctx = secp256k1::Secp256k1::new();
480 let signed_announcement = announcement
481 .sign(&ctx, &self.cfg.private.broadcast_secret_key.keypair(&ctx));
482
483 dbtx.insert_entry(
484 &ApiAnnouncementKey(self.cfg.local.identity),
485 &signed_announcement,
486 )
487 .await;
488
489 Result::<_, ()>::Ok(signed_announcement)
490 })
491 },
492 None,
493 )
494 .await
495 .expect("Will not terminate on error")
496 }
497}
498
499#[async_trait]
500impl HasApiContext<ConsensusApi> for ConsensusApi {
501 async fn context(
502 &self,
503 request: &ApiRequestErased,
504 id: Option<ModuleInstanceId>,
505 ) -> (&ConsensusApi, ApiEndpointContext<'_>) {
506 let mut db = self.db.clone();
507 let mut dbtx = self.db.begin_transaction().await;
508 if let Some(id) = id {
509 db = self.db.with_prefix_module_id(id).0;
510 dbtx = dbtx.with_prefix_module_id(id).0;
511 }
512 (
513 self,
514 ApiEndpointContext::new(
515 db,
516 dbtx,
517 request.auth == Some(self.cfg.private.api_auth.clone()),
518 request.auth.clone(),
519 ),
520 )
521 }
522}
523
524#[async_trait]
525impl HasApiContext<DynServerModule> for ConsensusApi {
526 async fn context(
527 &self,
528 request: &ApiRequestErased,
529 id: Option<ModuleInstanceId>,
530 ) -> (&DynServerModule, ApiEndpointContext<'_>) {
531 let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
532 (
533 self.modules.get_expect(id.expect("required module id")),
534 context,
535 )
536 }
537}
538
539#[async_trait]
540impl IDashboardApi for ConsensusApi {
541 async fn auth(&self) -> ApiAuth {
542 self.cfg.private.api_auth.clone()
543 }
544
545 async fn guardian_id(&self) -> PeerId {
546 self.cfg.local.identity
547 }
548
549 async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
550 self.cfg
551 .consensus
552 .api_endpoints()
553 .iter()
554 .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
555 .collect()
556 }
557
558 async fn federation_name(&self) -> String {
559 self.cfg
560 .consensus
561 .meta
562 .get(META_FEDERATION_NAME_KEY)
563 .cloned()
564 .expect("Federation name must be set")
565 }
566
567 async fn session_count(&self) -> u64 {
568 self.session_count().await
569 }
570
571 async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
572 self.session_status(session_idx).await
573 }
574
575 async fn consensus_ord_latency(&self) -> Option<Duration> {
576 *self.ord_latency_receiver.borrow()
577 }
578
579 async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<Duration>> {
580 self.p2p_status_receivers
581 .iter()
582 .map(|(peer, receiver)| (*peer, *receiver.borrow()))
583 .collect()
584 }
585
586 async fn federation_invite_code(&self) -> String {
587 self.cfg
588 .get_invite_code(self.get_active_api_secret())
589 .to_string()
590 }
591
592 async fn federation_audit(&self) -> AuditSummary {
593 self.get_federation_audit()
594 .await
595 .expect("Failed to get federation audit")
596 }
597
598 async fn bitcoin_rpc_url(&self) -> SafeUrl {
599 self.bitcoin_rpc_connection.url()
600 }
601
602 async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
603 self.bitcoin_rpc_connection.status()
604 }
605
606 fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
607 self.modules
608 .iter_modules()
609 .find_map(|(_, module_kind, module)| {
610 if *module_kind == kind {
611 Some(module)
612 } else {
613 None
614 }
615 })
616 }
617}
618
619pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
620 vec![
621 api_endpoint! {
622 VERSION_ENDPOINT,
623 ApiVersion::new(0, 0),
624 async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
625 Ok(fedimint.api_versions_summary().to_owned())
626 }
627 },
628 api_endpoint! {
629 SUBMIT_TRANSACTION_ENDPOINT,
630 ApiVersion::new(0, 0),
631 async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
632 let transaction = transaction
633 .try_into_inner(&fedimint.modules.decoder_registry())
634 .map_err(|e| ApiError::bad_request(e.to_string()))?;
635
636 Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
639 }
640 },
641 api_endpoint! {
642 AWAIT_TRANSACTION_ENDPOINT,
643 ApiVersion::new(0, 0),
644 async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
645 fedimint.await_transaction(tx_hash).await;
646
647 Ok(tx_hash)
648 }
649 },
650 api_endpoint! {
651 AWAIT_OUTPUT_OUTCOME_ENDPOINT,
652 ApiVersion::new(0, 0),
653 async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
654 let outcome = fedimint
655 .await_output_outcome(outpoint)
656 .await
657 .map_err(|e| ApiError::bad_request(e.to_string()))?;
658
659 Ok(outcome)
660 }
661 },
662 api_endpoint! {
663 INVITE_CODE_ENDPOINT,
664 ApiVersion::new(0, 0),
665 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
666 Ok(fedimint.cfg.get_invite_code(fedimint.get_active_api_secret()).to_string())
667 }
668 },
669 api_endpoint! {
670 FEDERATION_ID_ENDPOINT,
671 ApiVersion::new(0, 2),
672 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
673 Ok(fedimint.cfg.calculate_federation_id().to_string())
674 }
675 },
676 api_endpoint! {
677 CLIENT_CONFIG_ENDPOINT,
678 ApiVersion::new(0, 0),
679 async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
680 Ok(fedimint.client_cfg.clone())
681 }
682 },
683 api_endpoint! {
685 CLIENT_CONFIG_JSON_ENDPOINT,
686 ApiVersion::new(0, 0),
687 async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
688 Ok(fedimint.client_cfg.to_json())
689 }
690 },
691 api_endpoint! {
692 SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
693 ApiVersion::new(0, 0),
694 async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
695 Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
696 }
697 },
698 api_endpoint! {
699 STATUS_ENDPOINT,
700 ApiVersion::new(0, 0),
701 async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
702 Ok(StatusResponse {
703 server: ServerStatusLegacy::ConsensusRunning,
704 federation: Some(fedimint.get_federation_status().await?)
705 })}
706 },
707 api_endpoint! {
708 SETUP_STATUS_ENDPOINT,
709 ApiVersion::new(0, 0),
710 async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
711 Ok(SetupStatus::ConsensusIsRunning)
712 }
713 },
714 api_endpoint! {
715 CONSENSUS_ORD_LATENCY_ENDPOINT,
716 ApiVersion::new(0, 0),
717 async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
718 Ok(*fedimint.ord_latency_receiver.borrow())
719 }
720 },
721 api_endpoint! {
722 P2P_CONNECTION_STATUS_ENDPOINT,
723 ApiVersion::new(0, 0),
724 async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<Duration>> {
725 Ok(fedimint.p2p_status_receivers
726 .iter()
727 .map(|(peer, receiver)| (*peer, *receiver.borrow()))
728 .collect())
729 }
730 },
731 api_endpoint! {
732 SESSION_COUNT_ENDPOINT,
733 ApiVersion::new(0, 0),
734 async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
735 Ok(fedimint.session_count().await)
736 }
737 },
738 api_endpoint! {
739 AWAIT_SESSION_OUTCOME_ENDPOINT,
740 ApiVersion::new(0, 0),
741 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionOutcome> {
742 Ok((&fedimint.await_signed_session_outcome(index).await.session_outcome).into())
743 }
744 },
745 api_endpoint! {
746 AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
747 ApiVersion::new(0, 0),
748 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SignedSessionOutcome> {
749 Ok((&fedimint.await_signed_session_outcome(index).await).into())
750 }
751 },
752 api_endpoint! {
753 SESSION_STATUS_ENDPOINT,
754 ApiVersion::new(0, 1),
755 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
756 Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
757 }
758 },
759 api_endpoint! {
760 SESSION_STATUS_V2_ENDPOINT,
761 ApiVersion::new(0, 5),
762 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
763 Ok((&fedimint.session_status(index).await).into())
764 }
765 },
766 api_endpoint! {
767 SHUTDOWN_ENDPOINT,
768 ApiVersion::new(0, 3),
769 async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
770 check_auth(context)?;
771 fedimint.shutdown(index);
772 Ok(())
773 }
774 },
775 api_endpoint! {
776 AUDIT_ENDPOINT,
777 ApiVersion::new(0, 0),
778 async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
779 check_auth(context)?;
780 Ok(fedimint.get_federation_audit().await?)
781 }
782 },
783 api_endpoint! {
784 GUARDIAN_CONFIG_BACKUP_ENDPOINT,
785 ApiVersion::new(0, 2),
786 async |fedimint: &ConsensusApi, context, _v: ()| -> GuardianConfigBackup {
787 let auth = check_auth(context)?;
788 let password = context.request_auth().expect("Auth was checked before").0;
789 Ok(fedimint.get_guardian_config_backup(&password, &auth))
790 }
791 },
792 api_endpoint! {
793 BACKUP_ENDPOINT,
794 ApiVersion::new(0, 0),
795 async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
796 fedimint
797 .handle_backup_request(&mut context.dbtx().into_nc(), request).await?;
798 Ok(())
799
800 }
801 },
802 api_endpoint! {
803 RECOVER_ENDPOINT,
804 ApiVersion::new(0, 0),
805 async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
806 Ok(fedimint
807 .handle_recover_request(&mut context.dbtx().into_nc(), id).await)
808 }
809 },
810 api_endpoint! {
811 AUTH_ENDPOINT,
812 ApiVersion::new(0, 0),
813 async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
814 check_auth(context)?;
815 Ok(())
816 }
817 },
818 api_endpoint! {
819 API_ANNOUNCEMENTS_ENDPOINT,
820 ApiVersion::new(0, 3),
821 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
822 Ok(fedimint.api_announcements().await)
823 }
824 },
825 api_endpoint! {
826 SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
827 ApiVersion::new(0, 3),
828 async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
829 fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
830 }
831 },
832 api_endpoint! {
833 SIGN_API_ANNOUNCEMENT_ENDPOINT,
834 ApiVersion::new(0, 3),
835 async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
836 check_auth(context)?;
837 Ok(fedimint.sign_api_announcement(new_url).await)
838 }
839 },
840 api_endpoint! {
841 FEDIMINTD_VERSION_ENDPOINT,
842 ApiVersion::new(0, 4),
843 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
844 Ok(fedimint.fedimintd_version())
845 }
846 },
847 api_endpoint! {
848 BACKUP_STATISTICS_ENDPOINT,
849 ApiVersion::new(0, 5),
850 async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
851 check_auth(context)?;
852 Ok(backup_statistics_static(&mut context.dbtx().into_nc()).await)
853 }
854 },
855 ]
856}
857
858pub(crate) async fn backup_statistics_static(
859 dbtx: &mut DatabaseTransaction<'_>,
860) -> BackupStatistics {
861 const DAY_SECS: u64 = 24 * 60 * 60;
862 const WEEK_SECS: u64 = 7 * DAY_SECS;
863 const MONTH_SECS: u64 = 30 * DAY_SECS;
864 const QUARTER_SECS: u64 = 3 * MONTH_SECS;
865
866 let mut backup_stats = BackupStatistics::default();
867
868 let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
869 while let Some((_, backup)) = all_backups_stream.next().await {
870 backup_stats.num_backups += 1;
871 backup_stats.total_size += backup.data.len();
872
873 let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
874 if age_secs < DAY_SECS {
875 backup_stats.refreshed_1d += 1;
876 }
877 if age_secs < WEEK_SECS {
878 backup_stats.refreshed_1w += 1;
879 }
880 if age_secs < MONTH_SECS {
881 backup_stats.refreshed_1m += 1;
882 }
883 if age_secs < QUARTER_SECS {
884 backup_stats.refreshed_3m += 1;
885 }
886 }
887
888 backup_stats
889}