1use anyhow::{Context, Result};
2use nostr::nips::nip19::{FromBech32, ToBech32};
3use nostr::{Keys, SecretKey};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct Config {
10 #[serde(default)]
11 pub server: ServerConfig,
12 #[serde(default)]
13 pub storage: StorageConfig,
14 #[serde(default)]
15 pub nostr: NostrConfig,
16 #[serde(default)]
17 pub blossom: BlossomConfig,
18 #[serde(default)]
19 pub sync: SyncConfig,
20 #[serde(default)]
21 pub cashu: CashuConfig,
22 #[serde(default)]
23 pub updater: UpdaterConfig,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct UpdaterConfig {
33 #[serde(default = "default_auto_check")]
34 pub auto_check: bool,
35 #[serde(default)]
36 pub auto_install: bool,
37 #[serde(default = "default_check_interval_hours")]
38 pub check_interval_hours: u32,
39}
40
41impl Default for UpdaterConfig {
42 fn default() -> Self {
43 Self {
44 auto_check: default_auto_check(),
45 auto_install: false,
46 check_interval_hours: default_check_interval_hours(),
47 }
48 }
49}
50
51fn default_auto_check() -> bool {
52 true
53}
54
55fn default_check_interval_hours() -> u32 {
56 24
57}
58
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "kebab-case")]
61pub enum ServerMode {
62 #[default]
63 Normal,
64 #[serde(alias = "signal-only")]
65 Assist,
66}
67
68impl ServerMode {
69 pub const fn as_str(self) -> &'static str {
70 match self {
71 Self::Normal => "normal",
72 Self::Assist => "assist",
73 }
74 }
75
76 pub const fn hash_get_enabled(self) -> bool {
77 matches!(self, Self::Normal)
78 }
79
80 pub const fn background_services_enabled(self) -> bool {
81 matches!(self, Self::Normal)
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ServerConfig {
87 #[serde(default)]
88 pub mode: ServerMode,
89 #[serde(default = "default_bind_address")]
90 pub bind_address: String,
91 #[serde(default = "default_enable_auth")]
92 pub enable_auth: bool,
93 #[serde(default = "default_stun_port")]
95 pub stun_port: u16,
96 #[serde(default = "default_enable_webrtc")]
98 pub enable_webrtc: bool,
99 #[serde(default = "default_enable_fips")]
101 pub enable_fips: bool,
102 #[serde(default = "default_fips_discovery_scope")]
104 pub fips_discovery_scope: String,
105 #[serde(default)]
108 pub fips_relays: Vec<String>,
109 #[serde(default = "default_enable_fips_udp")]
111 pub enable_fips_udp: bool,
112 #[serde(default)]
114 pub fips_udp_bind_addr: Option<String>,
115 #[serde(default)]
117 pub fips_udp_public: bool,
118 #[serde(default)]
120 pub fips_udp_external_addr: Option<String>,
121 #[serde(default = "default_enable_fips_webrtc")]
123 pub enable_fips_webrtc: bool,
124 #[serde(default = "default_fetch_from_fips_peers", alias = "http_fips_fetch")]
126 pub fetch_from_fips_peers: bool,
127 #[serde(default = "default_fips_request_timeout_ms")]
129 pub fips_request_timeout_ms: u64,
130 #[serde(default = "default_http_webrtc_fetch")]
132 pub http_webrtc_fetch: bool,
133 #[serde(default, alias = "peer_direct_urls", alias = "peer_advertise_urls")]
136 pub peer_signal_urls: Vec<String>,
137 #[serde(default = "default_enable_multicast")]
139 pub enable_multicast: bool,
140 #[serde(default = "default_multicast_group")]
142 pub multicast_group: String,
143 #[serde(default = "default_multicast_port")]
145 pub multicast_port: u16,
146 #[serde(default = "default_max_multicast_peers")]
149 pub max_multicast_peers: usize,
150 #[serde(default = "default_enable_wifi_aware")]
152 pub enable_wifi_aware: bool,
153 #[serde(default = "default_max_wifi_aware_peers")]
156 pub max_wifi_aware_peers: usize,
157 #[serde(default = "default_enable_bluetooth")]
159 pub enable_bluetooth: bool,
160 #[serde(default = "default_max_bluetooth_peers")]
163 pub max_bluetooth_peers: usize,
164 #[serde(default = "default_public_writes")]
167 pub public_writes: bool,
168 #[serde(default = "default_public_plaintext_reads")]
171 pub public_plaintext_reads: bool,
172 #[serde(default = "default_socialgraph_snapshot_public")]
174 pub socialgraph_snapshot_public: bool,
175}
176
177fn default_public_writes() -> bool {
178 true
179}
180
181fn default_public_plaintext_reads() -> bool {
182 true
183}
184
185fn default_socialgraph_snapshot_public() -> bool {
186 false
187}
188
189impl ServerConfig {
190 pub fn resolved_fips_relays(&self, active_nostr_relays: &[String]) -> Vec<String> {
191 let configured = if self.fips_relays.is_empty() {
192 active_nostr_relays
193 } else {
194 &self.fips_relays
195 };
196 merge_fips_signal_relays(configured)
197 }
198}
199
200const DEFAULT_FIPS_SIGNAL_RELAYS: [&str; 2] = ["wss://temp.iris.to", "wss://relay.primal.net"];
201
202fn merge_fips_signal_relays(configured: &[String]) -> Vec<String> {
203 let mut relays = Vec::new();
204 for relay in configured
205 .iter()
206 .map(String::as_str)
207 .chain(DEFAULT_FIPS_SIGNAL_RELAYS)
208 {
209 let normalized = relay.trim().trim_end_matches('/').to_string();
210 if normalized.is_empty() || relays.contains(&normalized) {
211 continue;
212 }
213 relays.push(normalized);
214 }
215 relays
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct StorageConfig {
220 #[serde(default = "default_data_dir")]
221 pub data_dir: String,
222 #[serde(default = "default_max_size_gb")]
223 pub max_size_gb: u64,
224 #[serde(default = "default_storage_evict_orphans")]
225 pub evict_orphans: bool,
226 #[serde(default)]
228 pub s3: Option<S3Config>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct S3Config {
234 pub endpoint: String,
236 pub bucket: String,
238 #[serde(default)]
240 pub prefix: Option<String>,
241 #[serde(default = "default_s3_region")]
243 pub region: String,
244 #[serde(default)]
246 pub access_key: Option<String>,
247 #[serde(default)]
249 pub secret_key: Option<String>,
250 #[serde(default)]
252 pub public_url: Option<String>,
253}
254
255fn default_s3_region() -> String {
256 "auto".to_string()
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct NostrConfig {
261 #[serde(default = "default_nostr_enabled")]
262 pub enabled: bool,
263 #[serde(default = "default_relays")]
264 pub relays: Vec<String>,
265 #[serde(default)]
267 pub allowed_npubs: Vec<String>,
268 #[serde(default)]
270 pub socialgraph_root: Option<String>,
271 #[serde(default = "default_nostr_bootstrap_follows")]
274 pub bootstrap_follows: Vec<String>,
275 #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
277 pub social_graph_crawl_depth: u32,
278 #[serde(default)]
281 pub mirror_max_follow_distance: Option<u32>,
282 #[serde(default = "default_max_write_distance")]
284 pub max_write_distance: u32,
285 #[serde(default = "default_nostr_db_max_size_gb")]
287 pub db_max_size_gb: u64,
288 #[serde(default = "default_nostr_spambox_max_size_gb")]
291 pub spambox_max_size_gb: u64,
292 #[serde(default)]
294 pub negentropy_only: bool,
295 #[serde(default = "default_nostr_overmute_threshold")]
297 pub overmute_threshold: f64,
298 #[serde(default = "default_nostr_mirror_kinds")]
300 pub mirror_kinds: Vec<u16>,
301 #[serde(default = "default_nostr_history_sync_author_chunk_size")]
303 pub history_sync_author_chunk_size: usize,
304 #[serde(default = "default_nostr_history_sync_per_author_event_limit")]
306 pub history_sync_per_author_event_limit: usize,
307 #[serde(default = "default_nostr_history_sync_on_reconnect")]
309 pub history_sync_on_reconnect: bool,
310 #[serde(default = "default_nostr_full_text_note_history_follow_distance")]
313 pub full_text_note_history_follow_distance: Option<u32>,
314 #[serde(default = "default_nostr_full_text_note_history_max_relay_pages")]
317 pub full_text_note_history_max_relay_pages: usize,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct BlossomConfig {
322 #[serde(default = "default_blossom_enabled")]
323 pub enabled: bool,
324 #[serde(default)]
326 pub servers: Vec<String>,
327 #[serde(default = "default_read_servers")]
329 pub read_servers: Vec<String>,
330 #[serde(default = "default_write_servers")]
332 pub write_servers: Vec<String>,
333 #[serde(default = "default_max_upload_mb")]
335 pub max_upload_mb: u64,
336 #[serde(default = "default_require_random_untrusted_ingest")]
338 pub require_random_untrusted_ingest: bool,
339 #[serde(default = "default_optimistic_uploads")]
342 pub optimistic_uploads: bool,
343}
344
345impl BlossomConfig {
346 pub fn all_read_servers(&self) -> Vec<String> {
347 if !self.enabled {
348 return Vec::new();
349 }
350 let mut servers = self.servers.clone();
351 servers.extend(self.read_servers.clone());
352 servers.extend(self.write_servers.clone());
353 if servers.is_empty() {
354 servers = default_read_servers();
355 servers.extend(default_write_servers());
356 }
357 servers.sort();
358 servers.dedup();
359 servers
360 }
361
362 pub fn all_write_servers(&self) -> Vec<String> {
363 if !self.enabled {
364 return Vec::new();
365 }
366 let mut servers = self.servers.clone();
367 servers.extend(self.write_servers.clone());
368 if servers.is_empty() {
369 servers = default_write_servers();
370 }
371 servers.sort();
372 servers.dedup();
373 servers
374 }
375}
376
377impl NostrConfig {
378 pub fn active_relays(&self) -> Vec<String> {
379 if self.enabled {
380 self.relays.clone()
381 } else {
382 Vec::new()
383 }
384 }
385}
386
387fn default_read_servers() -> Vec<String> {
389 let mut servers = vec![
390 "https://blossom.primal.net".to_string(),
391 "https://cdn.iris.to".to_string(),
392 ];
393 servers.sort();
394 servers
395}
396
397fn default_write_servers() -> Vec<String> {
398 vec!["https://upload.iris.to".to_string()]
399}
400
401fn default_max_upload_mb() -> u64 {
402 5
403}
404
405fn default_require_random_untrusted_ingest() -> bool {
406 true
407}
408
409fn default_optimistic_uploads() -> bool {
410 false
411}
412
413fn default_nostr_enabled() -> bool {
414 true
415}
416
417fn default_blossom_enabled() -> bool {
418 true
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct SyncConfig {
423 #[serde(default = "default_sync_enabled")]
425 pub enabled: bool,
426 #[serde(default = "default_sync_own")]
428 pub sync_own: bool,
429 #[serde(default = "default_sync_followed")]
431 pub sync_followed: bool,
432 #[serde(default = "default_max_concurrent")]
434 pub max_concurrent: usize,
435 #[serde(default = "default_webrtc_timeout_ms")]
437 pub webrtc_timeout_ms: u64,
438 #[serde(default = "default_blossom_timeout_ms")]
440 pub blossom_timeout_ms: u64,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct CashuConfig {
445 #[serde(default)]
447 pub accepted_mints: Vec<String>,
448 #[serde(default)]
450 pub default_mint: Option<String>,
451 #[serde(default = "default_cashu_quote_payment_offer_sat")]
453 pub quote_payment_offer_sat: u64,
454 #[serde(default = "default_cashu_quote_ttl_ms")]
456 pub quote_ttl_ms: u32,
457 #[serde(default = "default_cashu_settlement_timeout_ms")]
459 pub settlement_timeout_ms: u64,
460 #[serde(default = "default_cashu_mint_failure_block_threshold")]
462 pub mint_failure_block_threshold: u64,
463 #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
465 pub peer_suggested_mint_base_cap_sat: u64,
466 #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
468 pub peer_suggested_mint_success_step_sat: u64,
469 #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
471 pub peer_suggested_mint_receipt_step_sat: u64,
472 #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
474 pub peer_suggested_mint_max_cap_sat: u64,
475 #[serde(default)]
477 pub payment_default_block_threshold: u64,
478 #[serde(default = "default_cashu_chunk_target_bytes")]
480 pub chunk_target_bytes: usize,
481}
482
483impl Default for CashuConfig {
484 fn default() -> Self {
485 Self {
486 accepted_mints: Vec::new(),
487 default_mint: None,
488 quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
489 quote_ttl_ms: default_cashu_quote_ttl_ms(),
490 settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
491 mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
492 peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
493 peer_suggested_mint_success_step_sat:
494 default_cashu_peer_suggested_mint_success_step_sat(),
495 peer_suggested_mint_receipt_step_sat:
496 default_cashu_peer_suggested_mint_receipt_step_sat(),
497 peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
498 payment_default_block_threshold: 0,
499 chunk_target_bytes: default_cashu_chunk_target_bytes(),
500 }
501 }
502}
503
504fn default_cashu_quote_payment_offer_sat() -> u64 {
505 3
506}
507
508fn default_cashu_quote_ttl_ms() -> u32 {
509 1_500
510}
511
512fn default_cashu_settlement_timeout_ms() -> u64 {
513 5_000
514}
515
516fn default_cashu_mint_failure_block_threshold() -> u64 {
517 2
518}
519
520fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
521 3
522}
523
524fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
525 1
526}
527
528fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
529 2
530}
531
532fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
533 21
534}
535
536fn default_cashu_chunk_target_bytes() -> usize {
537 32 * 1024
538}
539
540fn default_sync_enabled() -> bool {
541 true
542}
543
544fn default_sync_own() -> bool {
545 true
546}
547
548fn default_sync_followed() -> bool {
549 true
550}
551
552fn default_max_concurrent() -> usize {
553 3
554}
555
556fn default_webrtc_timeout_ms() -> u64 {
557 2000
558}
559
560fn default_blossom_timeout_ms() -> u64 {
561 10000
562}
563
564fn default_social_graph_crawl_depth() -> u32 {
565 2
566}
567
568fn default_nostr_bootstrap_follows() -> Vec<String> {
569 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
570}
571
572fn default_max_write_distance() -> u32 {
573 3
574}
575
576fn default_nostr_db_max_size_gb() -> u64 {
577 10
578}
579
580fn default_nostr_spambox_max_size_gb() -> u64 {
581 1
582}
583
584fn default_nostr_history_sync_on_reconnect() -> bool {
585 true
586}
587
588fn default_nostr_overmute_threshold() -> f64 {
589 1.0
590}
591
592fn default_nostr_mirror_kinds() -> Vec<u16> {
593 vec![0, 1, 3, 6, 7, 9_735, 30_023]
594}
595
596fn default_nostr_history_sync_author_chunk_size() -> usize {
597 5_000
598}
599
600fn default_nostr_history_sync_per_author_event_limit() -> usize {
601 256
602}
603
604fn default_nostr_full_text_note_history_follow_distance() -> Option<u32> {
605 Some(2)
606}
607
608fn default_nostr_full_text_note_history_max_relay_pages() -> usize {
609 0
610}
611
612fn default_relays() -> Vec<String> {
613 vec![
614 "wss://relay.damus.io".to_string(),
615 "wss://relay.snort.social".to_string(),
616 "wss://temp.iris.to".to_string(),
617 "wss://upload.iris.to/nostr".to_string(),
618 ]
619}
620
621fn default_bind_address() -> String {
622 "127.0.0.1:8080".to_string()
623}
624
625fn default_enable_auth() -> bool {
626 true
627}
628
629fn default_stun_port() -> u16 {
630 3478 }
632
633fn default_enable_webrtc() -> bool {
634 true
635}
636
637fn default_enable_fips() -> bool {
638 true
639}
640
641fn default_fips_discovery_scope() -> String {
642 "hashtree-v1".to_string()
643}
644
645fn default_enable_fips_udp() -> bool {
646 true
647}
648
649fn default_enable_fips_webrtc() -> bool {
650 true
651}
652
653fn default_fetch_from_fips_peers() -> bool {
654 true
655}
656
657fn default_fips_request_timeout_ms() -> u64 {
658 5_500
659}
660
661fn default_http_webrtc_fetch() -> bool {
662 true
663}
664
665fn default_enable_multicast() -> bool {
666 true
667}
668
669fn default_multicast_group() -> String {
670 "239.255.42.98".to_string()
671}
672
673fn default_multicast_port() -> u16 {
674 48555
675}
676
677fn default_max_multicast_peers() -> usize {
678 12
679}
680
681fn default_enable_wifi_aware() -> bool {
682 false
683}
684
685fn default_max_wifi_aware_peers() -> usize {
686 0
687}
688
689fn default_enable_bluetooth() -> bool {
690 false
691}
692
693fn default_max_bluetooth_peers() -> usize {
694 0
695}
696
697fn default_data_dir() -> String {
698 hashtree_config::get_hashtree_dir()
699 .join("data")
700 .to_string_lossy()
701 .to_string()
702}
703
704fn default_max_size_gb() -> u64 {
705 10
706}
707
708fn default_storage_evict_orphans() -> bool {
709 true
710}
711
712impl Default for ServerConfig {
713 fn default() -> Self {
714 Self {
715 mode: ServerMode::default(),
716 bind_address: default_bind_address(),
717 enable_auth: default_enable_auth(),
718 stun_port: default_stun_port(),
719 enable_webrtc: default_enable_webrtc(),
720 enable_fips: default_enable_fips(),
721 fips_discovery_scope: default_fips_discovery_scope(),
722 fips_relays: Vec::new(),
723 enable_fips_udp: default_enable_fips_udp(),
724 fips_udp_bind_addr: None,
725 fips_udp_public: false,
726 fips_udp_external_addr: None,
727 enable_fips_webrtc: default_enable_fips_webrtc(),
728 fetch_from_fips_peers: default_fetch_from_fips_peers(),
729 fips_request_timeout_ms: default_fips_request_timeout_ms(),
730 http_webrtc_fetch: default_http_webrtc_fetch(),
731 peer_signal_urls: Vec::new(),
732 enable_multicast: default_enable_multicast(),
733 multicast_group: default_multicast_group(),
734 multicast_port: default_multicast_port(),
735 max_multicast_peers: default_max_multicast_peers(),
736 enable_wifi_aware: default_enable_wifi_aware(),
737 max_wifi_aware_peers: default_max_wifi_aware_peers(),
738 enable_bluetooth: default_enable_bluetooth(),
739 max_bluetooth_peers: default_max_bluetooth_peers(),
740 public_writes: default_public_writes(),
741 public_plaintext_reads: default_public_plaintext_reads(),
742 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
743 }
744 }
745}
746
747impl Default for StorageConfig {
748 fn default() -> Self {
749 Self {
750 data_dir: default_data_dir(),
751 max_size_gb: default_max_size_gb(),
752 evict_orphans: default_storage_evict_orphans(),
753 s3: None,
754 }
755 }
756}
757
758impl Default for NostrConfig {
759 fn default() -> Self {
760 Self {
761 enabled: default_nostr_enabled(),
762 relays: default_relays(),
763 allowed_npubs: Vec::new(),
764 socialgraph_root: None,
765 bootstrap_follows: default_nostr_bootstrap_follows(),
766 social_graph_crawl_depth: default_social_graph_crawl_depth(),
767 mirror_max_follow_distance: None,
768 max_write_distance: default_max_write_distance(),
769 db_max_size_gb: default_nostr_db_max_size_gb(),
770 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
771 negentropy_only: false,
772 overmute_threshold: default_nostr_overmute_threshold(),
773 mirror_kinds: default_nostr_mirror_kinds(),
774 history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
775 history_sync_per_author_event_limit: default_nostr_history_sync_per_author_event_limit(
776 ),
777 history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
778 full_text_note_history_follow_distance:
779 default_nostr_full_text_note_history_follow_distance(),
780 full_text_note_history_max_relay_pages:
781 default_nostr_full_text_note_history_max_relay_pages(),
782 }
783 }
784}
785
786impl Default for BlossomConfig {
787 fn default() -> Self {
788 Self {
789 enabled: default_blossom_enabled(),
790 servers: Vec::new(),
791 read_servers: default_read_servers(),
792 write_servers: default_write_servers(),
793 max_upload_mb: default_max_upload_mb(),
794 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
795 optimistic_uploads: default_optimistic_uploads(),
796 }
797 }
798}
799
800impl Default for SyncConfig {
801 fn default() -> Self {
802 Self {
803 enabled: default_sync_enabled(),
804 sync_own: default_sync_own(),
805 sync_followed: default_sync_followed(),
806 max_concurrent: default_max_concurrent(),
807 webrtc_timeout_ms: default_webrtc_timeout_ms(),
808 blossom_timeout_ms: default_blossom_timeout_ms(),
809 }
810 }
811}
812
813impl Config {
814 pub fn load() -> Result<Self> {
816 let config_path = get_config_path();
817
818 if config_path.exists() {
819 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
820 toml::from_str(&content).context("Failed to parse config file")
821 } else {
822 let config = Config::default();
823 config.save()?;
824 Ok(config)
825 }
826 }
827
828 pub fn save(&self) -> Result<()> {
830 let config_path = get_config_path();
831
832 if let Some(parent) = config_path.parent() {
834 fs::create_dir_all(parent)?;
835 }
836
837 let content = toml::to_string_pretty(self)?;
838 fs::write(&config_path, content)?;
839
840 Ok(())
841 }
842}
843
844pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
846
847fn read_keys_from_path(keys_path: &Path) -> Result<Keys> {
848 let content = fs::read_to_string(keys_path).context("Failed to read keys file")?;
849 let entries = hashtree_config::parse_keys_file(&content);
850 let nsec_str = entries
851 .into_iter()
852 .next()
853 .map(|e| e.secret)
854 .context("Keys file is empty")?;
855 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
856 Ok(Keys::new(secret_key))
857}
858
859fn seed_identity_defaults_if_needed(data_dir: Option<&Path>, config: Option<&Config>) {
860 if let (Some(data_dir), Some(config)) = (data_dir, config) {
861 let _ = crate::bootstrap::seed_identity_defaults(data_dir, config);
862 }
863}
864
865fn write_keys_to_path(keys_path: &Path, keys: &Keys) -> Result<()> {
866 if let Some(parent) = keys_path.parent() {
867 fs::create_dir_all(parent)?;
868 }
869
870 let nsec = keys
871 .secret_key()
872 .to_bech32()
873 .context("Failed to encode nsec")?;
874 fs::write(keys_path, &nsec)?;
875
876 #[cfg(unix)]
877 {
878 use std::os::unix::fs::PermissionsExt;
879 let perms = fs::Permissions::from_mode(0o600);
880 fs::set_permissions(keys_path, perms)?;
881 }
882
883 Ok(())
884}
885
886pub fn ensure_auth_cookie() -> Result<(String, String)> {
888 let cookie_path = get_auth_cookie_path();
889
890 if cookie_path.exists() {
891 read_auth_cookie()
892 } else {
893 generate_auth_cookie()
894 }
895}
896
897pub fn read_auth_cookie() -> Result<(String, String)> {
899 let cookie_path = get_auth_cookie_path();
900 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
901
902 let parts: Vec<&str> = content.trim().split(':').collect();
903 if parts.len() != 2 {
904 anyhow::bail!("Invalid auth cookie format");
905 }
906
907 Ok((parts[0].to_string(), parts[1].to_string()))
908}
909
910pub fn ensure_keys() -> Result<(Keys, bool)> {
913 let config_dir = get_hashtree_dir();
914 let config = Config::load().ok();
915 let data_dir = config
916 .as_ref()
917 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
918 ensure_keys_in(&config_dir, data_dir, config.as_ref())
919}
920
921pub fn ensure_keys_in(
924 config_dir: &Path,
925 data_dir: Option<&Path>,
926 config: Option<&Config>,
927) -> Result<(Keys, bool)> {
928 let keys_path = config_dir.join("keys");
929
930 if keys_path.exists() {
931 Ok((read_keys_from_path(&keys_path)?, false))
932 } else {
933 let keys = generate_keys_in(config_dir, data_dir, config)?;
934 Ok((keys, true))
935 }
936}
937
938pub fn read_keys() -> Result<Keys> {
940 read_keys_in(&get_hashtree_dir())
941}
942
943pub fn read_keys_in(config_dir: &Path) -> Result<Keys> {
945 read_keys_from_path(&config_dir.join("keys"))
946}
947
948pub fn ensure_keys_string() -> Result<(String, bool)> {
951 let config_dir = get_hashtree_dir();
952 let config = Config::load().ok();
953 let data_dir = config
954 .as_ref()
955 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
956 ensure_keys_string_in(&config_dir, data_dir, config.as_ref())
957}
958
959pub fn ensure_keys_string_in(
962 config_dir: &Path,
963 data_dir: Option<&Path>,
964 config: Option<&Config>,
965) -> Result<(String, bool)> {
966 let keys_path = config_dir.join("keys");
967
968 if keys_path.exists() {
969 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
970 let entries = hashtree_config::parse_keys_file(&content);
971 let nsec_str = entries
972 .into_iter()
973 .next()
974 .map(|e| e.secret)
975 .context("Keys file is empty")?;
976 Ok((nsec_str, false))
977 } else {
978 let keys = generate_keys_in(config_dir, data_dir, config)?;
979 let nsec = keys
980 .secret_key()
981 .to_bech32()
982 .context("Failed to encode nsec")?;
983 Ok((nsec, true))
984 }
985}
986
987pub fn generate_keys() -> Result<Keys> {
989 let config_dir = get_hashtree_dir();
990 let config = Config::load().ok();
991 let data_dir = config
992 .as_ref()
993 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
994 generate_keys_in(&config_dir, data_dir, config.as_ref())
995}
996
997pub fn generate_keys_in(
1000 config_dir: &Path,
1001 data_dir: Option<&Path>,
1002 config: Option<&Config>,
1003) -> Result<Keys> {
1004 let keys = Keys::generate();
1005 write_keys_to_path(&config_dir.join("keys"), &keys)?;
1006 seed_identity_defaults_if_needed(data_dir, config);
1007 Ok(keys)
1008}
1009
1010pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
1012 keys.public_key().to_bytes()
1013}
1014
1015pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
1017 use nostr::PublicKey;
1018 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
1019 Ok(pk.to_bytes())
1020}
1021
1022pub fn generate_auth_cookie() -> Result<(String, String)> {
1024 use rand::Rng;
1025
1026 let cookie_path = get_auth_cookie_path();
1027
1028 if let Some(parent) = cookie_path.parent() {
1030 fs::create_dir_all(parent)?;
1031 }
1032
1033 let mut rng = rand::thread_rng();
1035 let username = format!("htree_{}", rng.gen::<u32>());
1036 let password: String = (0..32)
1037 .map(|_| {
1038 let idx = rng.gen_range(0..62);
1039 match idx {
1040 0..=25 => (b'a' + idx) as char,
1041 26..=51 => (b'A' + (idx - 26)) as char,
1042 _ => (b'0' + (idx - 52)) as char,
1043 }
1044 })
1045 .collect();
1046
1047 let content = format!("{}:{}", username, password);
1049 fs::write(&cookie_path, content)?;
1050
1051 #[cfg(unix)]
1053 {
1054 use std::os::unix::fs::PermissionsExt;
1055 let perms = fs::Permissions::from_mode(0o600);
1056 fs::set_permissions(&cookie_path, perms)?;
1057 }
1058
1059 Ok((username, password))
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064 use super::*;
1065 use crate::test_support::{test_env_lock, EnvVarGuard};
1066 use tempfile::TempDir;
1067
1068 #[test]
1069 fn test_config_default() {
1070 let config = Config::default();
1071 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
1072 assert!(config.server.enable_auth);
1073 assert!(config.server.enable_multicast);
1074 assert_eq!(config.server.multicast_group, "239.255.42.98");
1075 assert_eq!(config.server.multicast_port, 48555);
1076 assert_eq!(config.server.max_multicast_peers, 12);
1077 assert!(!config.server.enable_wifi_aware);
1078 assert_eq!(config.server.max_wifi_aware_peers, 0);
1079 assert!(!config.server.enable_bluetooth);
1080 assert_eq!(config.server.max_bluetooth_peers, 0);
1081 assert!(config.server.public_plaintext_reads);
1082 assert_eq!(config.storage.max_size_gb, 10);
1083 assert!(config.storage.evict_orphans);
1084 assert!(config.nostr.enabled);
1085 assert!(config
1086 .nostr
1087 .relays
1088 .contains(&"wss://upload.iris.to/nostr".to_string()));
1089 assert!(config.blossom.enabled);
1090 assert!(!config.blossom.optimistic_uploads);
1091 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1092 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1093 assert_eq!(config.nostr.max_write_distance, 3);
1094 assert_eq!(config.nostr.db_max_size_gb, 10);
1095 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1096 assert!(!config.nostr.negentropy_only);
1097 assert_eq!(config.nostr.overmute_threshold, 1.0);
1098 assert_eq!(
1099 config.nostr.mirror_kinds,
1100 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1101 );
1102 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1103 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1104 assert!(config.nostr.history_sync_on_reconnect);
1105 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1106 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1107 assert!(config.nostr.socialgraph_root.is_none());
1108 assert_eq!(
1109 config.nostr.bootstrap_follows,
1110 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1111 );
1112 assert!(!config.server.socialgraph_snapshot_public);
1113 assert!(config.cashu.accepted_mints.is_empty());
1114 assert!(config.cashu.default_mint.is_none());
1115 assert_eq!(config.cashu.quote_payment_offer_sat, 3);
1116 assert_eq!(config.cashu.quote_ttl_ms, 1_500);
1117 assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
1118 assert_eq!(config.cashu.mint_failure_block_threshold, 2);
1119 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
1120 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
1121 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
1122 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
1123 assert_eq!(config.cashu.payment_default_block_threshold, 0);
1124 assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
1125 }
1126
1127 #[test]
1128 fn test_blossom_optimistic_uploads_deserialize() {
1129 let toml_str = r#"
1130[blossom]
1131optimistic_uploads = true
1132"#;
1133 let config: Config = toml::from_str(toml_str).unwrap();
1134 assert!(config.blossom.optimistic_uploads);
1135 assert!(config.blossom.require_random_untrusted_ingest);
1136 }
1137
1138 #[test]
1139 fn test_server_public_plaintext_reads_deserialize() {
1140 let toml_str = r#"
1141[server]
1142public_plaintext_reads = false
1143"#;
1144 let config: Config = toml::from_str(toml_str).unwrap();
1145 assert!(!config.server.public_plaintext_reads);
1146 assert!(config.server.public_writes);
1147 }
1148
1149 #[test]
1150 fn test_nostr_config_deserialize_with_defaults() {
1151 let toml_str = r#"
1152[nostr]
1153relays = ["wss://relay.damus.io"]
1154"#;
1155 let config: Config = toml::from_str(toml_str).unwrap();
1156 assert!(config.nostr.enabled);
1157 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
1158 assert!(config.storage.evict_orphans);
1159 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1160 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1161 assert_eq!(config.nostr.max_write_distance, 3);
1162 assert_eq!(config.nostr.db_max_size_gb, 10);
1163 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1164 assert!(!config.nostr.negentropy_only);
1165 assert_eq!(config.nostr.overmute_threshold, 1.0);
1166 assert_eq!(
1167 config.nostr.mirror_kinds,
1168 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1169 );
1170 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1171 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1172 assert!(config.nostr.history_sync_on_reconnect);
1173 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1174 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1175 assert!(config.nostr.socialgraph_root.is_none());
1176 assert_eq!(
1177 config.nostr.bootstrap_follows,
1178 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1179 );
1180 }
1181
1182 #[test]
1183 fn test_nostr_config_deserialize_with_socialgraph() {
1184 let toml_str = r#"
1185[nostr]
1186relays = ["wss://relay.damus.io"]
1187socialgraph_root = "npub1test"
1188bootstrap_follows = []
1189social_graph_crawl_depth = 3
1190mirror_max_follow_distance = 2
1191max_write_distance = 5
1192negentropy_only = true
1193overmute_threshold = 2.5
1194mirror_kinds = [0, 10000]
1195history_sync_author_chunk_size = 250
1196history_sync_per_author_event_limit = 128
1197history_sync_on_reconnect = false
1198full_text_note_history_follow_distance = 1
1199full_text_note_history_max_relay_pages = 64
1200"#;
1201 let config: Config = toml::from_str(toml_str).unwrap();
1202 assert!(config.nostr.enabled);
1203 assert!(config.storage.evict_orphans);
1204 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
1205 assert!(config.nostr.bootstrap_follows.is_empty());
1206 assert_eq!(config.nostr.social_graph_crawl_depth, 3);
1207 assert_eq!(config.nostr.mirror_max_follow_distance, Some(2));
1208 assert_eq!(config.nostr.max_write_distance, 5);
1209 assert_eq!(config.nostr.db_max_size_gb, 10);
1210 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1211 assert!(config.nostr.negentropy_only);
1212 assert_eq!(config.nostr.overmute_threshold, 2.5);
1213 assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
1214 assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
1215 assert_eq!(config.nostr.history_sync_per_author_event_limit, 128);
1216 assert!(!config.nostr.history_sync_on_reconnect);
1217 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(1));
1218 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 64);
1219 }
1220
1221 #[test]
1222 fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
1223 let toml_str = r#"
1224[nostr]
1225relays = ["wss://relay.damus.io"]
1226crawl_depth = 4
1227"#;
1228 let config: Config = toml::from_str(toml_str).unwrap();
1229 assert_eq!(config.nostr.social_graph_crawl_depth, 4);
1230 }
1231
1232 #[test]
1233 fn test_storage_config_disables_orphan_eviction_when_requested() {
1234 let toml_str = r#"
1235[storage]
1236evict_orphans = false
1237"#;
1238 let config: Config = toml::from_str(toml_str).unwrap();
1239 assert!(!config.storage.evict_orphans);
1240 }
1241
1242 #[test]
1243 fn test_server_config_deserialize_with_multicast() {
1244 let toml_str = r#"
1245[server]
1246enable_multicast = true
1247multicast_group = "239.255.42.99"
1248multicast_port = 49001
1249max_multicast_peers = 12
1250enable_wifi_aware = true
1251max_wifi_aware_peers = 5
1252enable_bluetooth = true
1253max_bluetooth_peers = 6
1254"#;
1255 let config: Config = toml::from_str(toml_str).unwrap();
1256 assert!(config.server.enable_multicast);
1257 assert_eq!(config.server.multicast_group, "239.255.42.99");
1258 assert_eq!(config.server.multicast_port, 49_001);
1259 assert_eq!(config.server.max_multicast_peers, 12);
1260 assert!(config.server.enable_wifi_aware);
1261 assert_eq!(config.server.max_wifi_aware_peers, 5);
1262 assert!(config.server.enable_bluetooth);
1263 assert_eq!(config.server.max_bluetooth_peers, 6);
1264 }
1265
1266 #[test]
1267 fn test_cashu_config_deserialize_with_accepted_mints() {
1268 let toml_str = r#"
1269[cashu]
1270accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
1271default_mint = "https://mint1.example"
1272quote_payment_offer_sat = 5
1273quote_ttl_ms = 2500
1274settlement_timeout_ms = 7000
1275mint_failure_block_threshold = 3
1276peer_suggested_mint_base_cap_sat = 4
1277peer_suggested_mint_success_step_sat = 2
1278peer_suggested_mint_receipt_step_sat = 3
1279peer_suggested_mint_max_cap_sat = 34
1280payment_default_block_threshold = 2
1281chunk_target_bytes = 65536
1282"#;
1283 let config: Config = toml::from_str(toml_str).unwrap();
1284 assert_eq!(
1285 config.cashu.accepted_mints,
1286 vec![
1287 "https://mint1.example".to_string(),
1288 "http://127.0.0.1:3338".to_string()
1289 ]
1290 );
1291 assert_eq!(
1292 config.cashu.default_mint,
1293 Some("https://mint1.example".to_string())
1294 );
1295 assert_eq!(config.cashu.quote_payment_offer_sat, 5);
1296 assert_eq!(config.cashu.quote_ttl_ms, 2500);
1297 assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
1298 assert_eq!(config.cashu.mint_failure_block_threshold, 3);
1299 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
1300 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
1301 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
1302 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
1303 assert_eq!(config.cashu.payment_default_block_threshold, 2);
1304 assert_eq!(config.cashu.chunk_target_bytes, 65_536);
1305 }
1306
1307 #[test]
1308 fn test_auth_cookie_generation() -> Result<()> {
1309 let _lock = test_env_lock()
1310 .lock()
1311 .unwrap_or_else(|err| err.into_inner());
1312 let temp_dir = TempDir::new()?;
1313 let _guard = EnvVarGuard::set("HTREE_CONFIG_DIR", temp_dir.path());
1314
1315 let (username, password) = generate_auth_cookie()?;
1316
1317 assert!(username.starts_with("htree_"));
1318 assert_eq!(password.len(), 32);
1319
1320 let cookie_path = get_auth_cookie_path();
1322 assert!(cookie_path.exists());
1323
1324 let (u2, p2) = read_auth_cookie()?;
1326 assert_eq!(username, u2);
1327 assert_eq!(password, p2);
1328
1329 Ok(())
1330 }
1331
1332 #[test]
1333 fn test_blossom_read_servers_include_write_only_servers_as_fresh_fallbacks() {
1334 let mut config = BlossomConfig::default();
1335 config.servers = vec!["https://legacy.server".to_string()];
1336
1337 let read = config.all_read_servers();
1338 assert!(read.contains(&"https://legacy.server".to_string()));
1339 assert!(read.contains(&"https://cdn.iris.to".to_string()));
1340 assert!(read.contains(&"https://blossom.primal.net".to_string()));
1341 assert!(read.contains(&"https://upload.iris.to".to_string()));
1342
1343 let write = config.all_write_servers();
1344 assert!(write.contains(&"https://legacy.server".to_string()));
1345 assert!(write.contains(&"https://upload.iris.to".to_string()));
1346 }
1347
1348 #[test]
1349 fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1350 let config = BlossomConfig {
1351 enabled: true,
1352 servers: Vec::new(),
1353 read_servers: Vec::new(),
1354 write_servers: Vec::new(),
1355 max_upload_mb: default_max_upload_mb(),
1356 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1357 optimistic_uploads: default_optimistic_uploads(),
1358 };
1359
1360 let read = config.all_read_servers();
1361 let mut expected = default_read_servers();
1362 expected.extend(default_write_servers());
1363 expected.sort();
1364 expected.dedup();
1365 assert_eq!(read, expected);
1366
1367 let write = config.all_write_servers();
1368 assert_eq!(write, default_write_servers());
1369 }
1370
1371 #[test]
1372 fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1373 let nostr = NostrConfig {
1374 enabled: false,
1375 relays: vec!["wss://relay.example".to_string()],
1376 ..NostrConfig::default()
1377 };
1378 assert!(nostr.active_relays().is_empty());
1379
1380 let blossom = BlossomConfig {
1381 enabled: false,
1382 servers: vec!["https://legacy.server".to_string()],
1383 read_servers: vec!["https://read.example".to_string()],
1384 write_servers: vec!["https://write.example".to_string()],
1385 max_upload_mb: default_max_upload_mb(),
1386 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1387 optimistic_uploads: default_optimistic_uploads(),
1388 };
1389 assert!(blossom.all_read_servers().is_empty());
1390 assert!(blossom.all_write_servers().is_empty());
1391 }
1392
1393 #[test]
1394 fn server_defaults_enable_fips_udp_and_webrtc() {
1395 let server = ServerConfig::default();
1396
1397 assert!(server.enable_fips);
1398 assert!(server.enable_fips_udp);
1399 assert!(server.fips_udp_bind_addr.is_none());
1400 assert!(!server.fips_udp_public);
1401 assert!(server.fips_udp_external_addr.is_none());
1402 assert!(server.enable_fips_webrtc);
1403 assert!(server.fetch_from_fips_peers);
1404 assert!(server.fips_relays.is_empty());
1405 assert_eq!(server.fips_discovery_scope, "hashtree-v1");
1406 assert_eq!(server.fips_request_timeout_ms, 5_500);
1407 }
1408
1409 #[test]
1410 fn server_config_reads_fips_overrides() {
1411 let config: Config = toml::from_str(
1412 r#"
1413[server]
1414enable_fips = true
1415fips_discovery_scope = "test-hashtree"
1416fips_relays = ["wss://fips.example"]
1417enable_fips_udp = false
1418fips_udp_bind_addr = "0.0.0.0:2121"
1419fips_udp_public = true
1420fips_udp_external_addr = "198.19.77.10:2121"
1421enable_fips_webrtc = true
1422fetch_from_fips_peers = false
1423fips_request_timeout_ms = 42
1424"#,
1425 )
1426 .unwrap();
1427
1428 assert!(config.server.enable_fips);
1429 assert_eq!(config.server.fips_discovery_scope, "test-hashtree");
1430 assert_eq!(config.server.fips_relays, ["wss://fips.example"]);
1431 assert!(!config.server.enable_fips_udp);
1432 assert_eq!(
1433 config.server.fips_udp_bind_addr.as_deref(),
1434 Some("0.0.0.0:2121")
1435 );
1436 assert!(config.server.fips_udp_public);
1437 assert_eq!(
1438 config.server.fips_udp_external_addr.as_deref(),
1439 Some("198.19.77.10:2121")
1440 );
1441 assert!(config.server.enable_fips_webrtc);
1442 assert!(!config.server.fetch_from_fips_peers);
1443 assert_eq!(config.server.fips_request_timeout_ms, 42);
1444 }
1445
1446 #[test]
1447 fn server_config_accepts_legacy_http_fips_fetch_name() {
1448 let config: Config = toml::from_str(
1449 r#"
1450[server]
1451http_fips_fetch = false
1452"#,
1453 )
1454 .unwrap();
1455
1456 assert!(!config.server.fetch_from_fips_peers);
1457 }
1458
1459 #[test]
1460 fn fips_relay_resolution_prefers_fips_relays_then_nostr() {
1461 let active_nostr = vec!["wss://nostr.example".to_string()];
1462 let mut server = ServerConfig::default();
1463
1464 assert_eq!(
1465 server.resolved_fips_relays(&active_nostr),
1466 [
1467 "wss://nostr.example",
1468 "wss://temp.iris.to",
1469 "wss://relay.primal.net"
1470 ]
1471 );
1472
1473 server.fips_relays = vec!["wss://fips.example".to_string()];
1474 assert_eq!(
1475 server.resolved_fips_relays(&["wss://ignored.example".to_string()]),
1476 [
1477 "wss://fips.example",
1478 "wss://temp.iris.to",
1479 "wss://relay.primal.net"
1480 ]
1481 );
1482 }
1483
1484 #[test]
1485 fn fips_relay_resolution_dedupes_bootstrap_relays() {
1486 let mut server = ServerConfig::default();
1487 server.fips_relays = vec![
1488 "wss://temp.iris.to/".to_string(),
1489 " wss://relay.primal.net ".to_string(),
1490 "wss://extra.example".to_string(),
1491 ];
1492
1493 assert_eq!(
1494 server.resolved_fips_relays(&[]),
1495 [
1496 "wss://temp.iris.to",
1497 "wss://relay.primal.net",
1498 "wss://extra.example"
1499 ]
1500 );
1501 }
1502}