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