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