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