1use anyhow::{Context, Result};
2use nostr::nips::nip19::{FromBech32, ToBech32};
3use nostr::{Keys, SecretKey};
4use serde::{Deserialize, Serialize};
5use std::fs;
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct Config {
9 #[serde(default)]
10 pub server: ServerConfig,
11 #[serde(default)]
12 pub storage: StorageConfig,
13 #[serde(default)]
14 pub nostr: NostrConfig,
15 #[serde(default)]
16 pub blossom: BlossomConfig,
17 #[serde(default)]
18 pub sync: SyncConfig,
19 #[serde(default)]
20 pub cashu: CashuConfig,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ServerConfig {
25 #[serde(default = "default_bind_address")]
26 pub bind_address: String,
27 #[serde(default = "default_enable_auth")]
28 pub enable_auth: bool,
29 #[serde(default = "default_stun_port")]
31 pub stun_port: u16,
32 #[serde(default = "default_enable_webrtc")]
34 pub enable_webrtc: bool,
35 #[serde(default = "default_enable_multicast")]
37 pub enable_multicast: bool,
38 #[serde(default = "default_multicast_group")]
40 pub multicast_group: String,
41 #[serde(default = "default_multicast_port")]
43 pub multicast_port: u16,
44 #[serde(default = "default_max_multicast_peers")]
47 pub max_multicast_peers: usize,
48 #[serde(default = "default_enable_wifi_aware")]
50 pub enable_wifi_aware: bool,
51 #[serde(default = "default_max_wifi_aware_peers")]
54 pub max_wifi_aware_peers: usize,
55 #[serde(default = "default_enable_bluetooth")]
57 pub enable_bluetooth: bool,
58 #[serde(default = "default_max_bluetooth_peers")]
61 pub max_bluetooth_peers: usize,
62 #[serde(default = "default_public_writes")]
65 pub public_writes: bool,
66 #[serde(default = "default_socialgraph_snapshot_public")]
68 pub socialgraph_snapshot_public: bool,
69}
70
71fn default_public_writes() -> bool {
72 true
73}
74
75fn default_socialgraph_snapshot_public() -> bool {
76 false
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct StorageConfig {
81 #[serde(default = "default_data_dir")]
82 pub data_dir: String,
83 #[serde(default = "default_max_size_gb")]
84 pub max_size_gb: u64,
85 #[serde(default)]
87 pub s3: Option<S3Config>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct S3Config {
93 pub endpoint: String,
95 pub bucket: String,
97 #[serde(default)]
99 pub prefix: Option<String>,
100 #[serde(default = "default_s3_region")]
102 pub region: String,
103 #[serde(default)]
105 pub access_key: Option<String>,
106 #[serde(default)]
108 pub secret_key: Option<String>,
109 #[serde(default)]
111 pub public_url: Option<String>,
112}
113
114fn default_s3_region() -> String {
115 "auto".to_string()
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct NostrConfig {
120 #[serde(default = "default_nostr_enabled")]
121 pub enabled: bool,
122 #[serde(default = "default_relays")]
123 pub relays: Vec<String>,
124 #[serde(default)]
126 pub allowed_npubs: Vec<String>,
127 #[serde(default)]
129 pub socialgraph_root: Option<String>,
130 #[serde(default = "default_nostr_bootstrap_follows")]
133 pub bootstrap_follows: Vec<String>,
134 #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
136 pub social_graph_crawl_depth: u32,
137 #[serde(default = "default_max_write_distance")]
139 pub max_write_distance: u32,
140 #[serde(default = "default_nostr_db_max_size_gb")]
142 pub db_max_size_gb: u64,
143 #[serde(default = "default_nostr_spambox_max_size_gb")]
146 pub spambox_max_size_gb: u64,
147 #[serde(default)]
149 pub negentropy_only: bool,
150 #[serde(default = "default_nostr_overmute_threshold")]
152 pub overmute_threshold: f64,
153 #[serde(default = "default_nostr_mirror_kinds")]
155 pub mirror_kinds: Vec<u16>,
156 #[serde(default = "default_nostr_history_sync_author_chunk_size")]
158 pub history_sync_author_chunk_size: usize,
159 #[serde(default = "default_nostr_history_sync_on_reconnect")]
161 pub history_sync_on_reconnect: bool,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct BlossomConfig {
166 #[serde(default = "default_blossom_enabled")]
167 pub enabled: bool,
168 #[serde(default)]
170 pub servers: Vec<String>,
171 #[serde(default = "default_read_servers")]
173 pub read_servers: Vec<String>,
174 #[serde(default = "default_write_servers")]
176 pub write_servers: Vec<String>,
177 #[serde(default = "default_max_upload_mb")]
179 pub max_upload_mb: u64,
180}
181
182impl BlossomConfig {
183 pub fn all_read_servers(&self) -> Vec<String> {
184 if !self.enabled {
185 return Vec::new();
186 }
187 let mut servers = self.servers.clone();
188 servers.extend(self.read_servers.clone());
189 servers.extend(self.write_servers.clone());
190 if servers.is_empty() {
191 servers = default_read_servers();
192 servers.extend(default_write_servers());
193 }
194 servers.sort();
195 servers.dedup();
196 servers
197 }
198
199 pub fn all_write_servers(&self) -> Vec<String> {
200 if !self.enabled {
201 return Vec::new();
202 }
203 let mut servers = self.servers.clone();
204 servers.extend(self.write_servers.clone());
205 if servers.is_empty() {
206 servers = default_write_servers();
207 }
208 servers.sort();
209 servers.dedup();
210 servers
211 }
212}
213
214impl NostrConfig {
215 pub fn active_relays(&self) -> Vec<String> {
216 if self.enabled {
217 self.relays.clone()
218 } else {
219 Vec::new()
220 }
221 }
222}
223
224fn default_read_servers() -> Vec<String> {
226 let mut servers = vec![
227 "https://blossom.primal.net".to_string(),
228 "https://cdn.iris.to".to_string(),
229 "https://hashtree.iris.to".to_string(),
230 ];
231 servers.sort();
232 servers
233}
234
235fn default_write_servers() -> Vec<String> {
236 vec!["https://upload.iris.to".to_string()]
237}
238
239fn default_max_upload_mb() -> u64 {
240 5
241}
242
243fn default_nostr_enabled() -> bool {
244 true
245}
246
247fn default_blossom_enabled() -> bool {
248 true
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct SyncConfig {
253 #[serde(default = "default_sync_enabled")]
255 pub enabled: bool,
256 #[serde(default = "default_sync_own")]
258 pub sync_own: bool,
259 #[serde(default = "default_sync_followed")]
261 pub sync_followed: bool,
262 #[serde(default = "default_max_concurrent")]
264 pub max_concurrent: usize,
265 #[serde(default = "default_webrtc_timeout_ms")]
267 pub webrtc_timeout_ms: u64,
268 #[serde(default = "default_blossom_timeout_ms")]
270 pub blossom_timeout_ms: u64,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct CashuConfig {
275 #[serde(default)]
277 pub accepted_mints: Vec<String>,
278 #[serde(default)]
280 pub default_mint: Option<String>,
281 #[serde(default = "default_cashu_quote_payment_offer_sat")]
283 pub quote_payment_offer_sat: u64,
284 #[serde(default = "default_cashu_quote_ttl_ms")]
286 pub quote_ttl_ms: u32,
287 #[serde(default = "default_cashu_settlement_timeout_ms")]
289 pub settlement_timeout_ms: u64,
290 #[serde(default = "default_cashu_mint_failure_block_threshold")]
292 pub mint_failure_block_threshold: u64,
293 #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
295 pub peer_suggested_mint_base_cap_sat: u64,
296 #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
298 pub peer_suggested_mint_success_step_sat: u64,
299 #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
301 pub peer_suggested_mint_receipt_step_sat: u64,
302 #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
304 pub peer_suggested_mint_max_cap_sat: u64,
305 #[serde(default)]
307 pub payment_default_block_threshold: u64,
308 #[serde(default = "default_cashu_chunk_target_bytes")]
310 pub chunk_target_bytes: usize,
311}
312
313impl Default for CashuConfig {
314 fn default() -> Self {
315 Self {
316 accepted_mints: Vec::new(),
317 default_mint: None,
318 quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
319 quote_ttl_ms: default_cashu_quote_ttl_ms(),
320 settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
321 mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
322 peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
323 peer_suggested_mint_success_step_sat:
324 default_cashu_peer_suggested_mint_success_step_sat(),
325 peer_suggested_mint_receipt_step_sat:
326 default_cashu_peer_suggested_mint_receipt_step_sat(),
327 peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
328 payment_default_block_threshold: 0,
329 chunk_target_bytes: default_cashu_chunk_target_bytes(),
330 }
331 }
332}
333
334fn default_cashu_quote_payment_offer_sat() -> u64 {
335 3
336}
337
338fn default_cashu_quote_ttl_ms() -> u32 {
339 1_500
340}
341
342fn default_cashu_settlement_timeout_ms() -> u64 {
343 5_000
344}
345
346fn default_cashu_mint_failure_block_threshold() -> u64 {
347 2
348}
349
350fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
351 3
352}
353
354fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
355 1
356}
357
358fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
359 2
360}
361
362fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
363 21
364}
365
366fn default_cashu_chunk_target_bytes() -> usize {
367 32 * 1024
368}
369
370fn default_sync_enabled() -> bool {
371 true
372}
373
374fn default_sync_own() -> bool {
375 true
376}
377
378fn default_sync_followed() -> bool {
379 true
380}
381
382fn default_max_concurrent() -> usize {
383 3
384}
385
386fn default_webrtc_timeout_ms() -> u64 {
387 2000
388}
389
390fn default_blossom_timeout_ms() -> u64 {
391 10000
392}
393
394fn default_social_graph_crawl_depth() -> u32 {
395 2
396}
397
398fn default_nostr_bootstrap_follows() -> Vec<String> {
399 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
400}
401
402fn default_max_write_distance() -> u32 {
403 3
404}
405
406fn default_nostr_db_max_size_gb() -> u64 {
407 10
408}
409
410fn default_nostr_spambox_max_size_gb() -> u64 {
411 1
412}
413
414fn default_nostr_history_sync_on_reconnect() -> bool {
415 true
416}
417
418fn default_nostr_overmute_threshold() -> f64 {
419 1.0
420}
421
422fn default_nostr_mirror_kinds() -> Vec<u16> {
423 vec![0, 3]
424}
425
426fn default_nostr_history_sync_author_chunk_size() -> usize {
427 5_000
428}
429
430fn default_relays() -> Vec<String> {
431 vec![
432 "wss://relay.damus.io".to_string(),
433 "wss://relay.snort.social".to_string(),
434 "wss://temp.iris.to".to_string(),
435 "wss://upload.iris.to/nostr".to_string(),
436 ]
437}
438
439fn default_bind_address() -> String {
440 "127.0.0.1:8080".to_string()
441}
442
443fn default_enable_auth() -> bool {
444 true
445}
446
447fn default_stun_port() -> u16 {
448 3478 }
450
451fn default_enable_webrtc() -> bool {
452 true
453}
454
455fn default_enable_multicast() -> bool {
456 true
457}
458
459fn default_multicast_group() -> String {
460 "239.255.42.98".to_string()
461}
462
463fn default_multicast_port() -> u16 {
464 48555
465}
466
467fn default_max_multicast_peers() -> usize {
468 12
469}
470
471fn default_enable_wifi_aware() -> bool {
472 false
473}
474
475fn default_max_wifi_aware_peers() -> usize {
476 0
477}
478
479fn default_enable_bluetooth() -> bool {
480 false
481}
482
483fn default_max_bluetooth_peers() -> usize {
484 0
485}
486
487fn default_data_dir() -> String {
488 hashtree_config::get_hashtree_dir()
489 .join("data")
490 .to_string_lossy()
491 .to_string()
492}
493
494fn default_max_size_gb() -> u64 {
495 10
496}
497
498impl Default for ServerConfig {
499 fn default() -> Self {
500 Self {
501 bind_address: default_bind_address(),
502 enable_auth: default_enable_auth(),
503 stun_port: default_stun_port(),
504 enable_webrtc: default_enable_webrtc(),
505 enable_multicast: default_enable_multicast(),
506 multicast_group: default_multicast_group(),
507 multicast_port: default_multicast_port(),
508 max_multicast_peers: default_max_multicast_peers(),
509 enable_wifi_aware: default_enable_wifi_aware(),
510 max_wifi_aware_peers: default_max_wifi_aware_peers(),
511 enable_bluetooth: default_enable_bluetooth(),
512 max_bluetooth_peers: default_max_bluetooth_peers(),
513 public_writes: default_public_writes(),
514 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
515 }
516 }
517}
518
519impl Default for StorageConfig {
520 fn default() -> Self {
521 Self {
522 data_dir: default_data_dir(),
523 max_size_gb: default_max_size_gb(),
524 s3: None,
525 }
526 }
527}
528
529impl Default for NostrConfig {
530 fn default() -> Self {
531 Self {
532 enabled: default_nostr_enabled(),
533 relays: default_relays(),
534 allowed_npubs: Vec::new(),
535 socialgraph_root: None,
536 bootstrap_follows: default_nostr_bootstrap_follows(),
537 social_graph_crawl_depth: default_social_graph_crawl_depth(),
538 max_write_distance: default_max_write_distance(),
539 db_max_size_gb: default_nostr_db_max_size_gb(),
540 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
541 negentropy_only: false,
542 overmute_threshold: default_nostr_overmute_threshold(),
543 mirror_kinds: default_nostr_mirror_kinds(),
544 history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
545 history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
546 }
547 }
548}
549
550impl Default for BlossomConfig {
551 fn default() -> Self {
552 Self {
553 enabled: default_blossom_enabled(),
554 servers: Vec::new(),
555 read_servers: default_read_servers(),
556 write_servers: default_write_servers(),
557 max_upload_mb: default_max_upload_mb(),
558 }
559 }
560}
561
562impl Default for SyncConfig {
563 fn default() -> Self {
564 Self {
565 enabled: default_sync_enabled(),
566 sync_own: default_sync_own(),
567 sync_followed: default_sync_followed(),
568 max_concurrent: default_max_concurrent(),
569 webrtc_timeout_ms: default_webrtc_timeout_ms(),
570 blossom_timeout_ms: default_blossom_timeout_ms(),
571 }
572 }
573}
574
575impl Config {
576 pub fn load() -> Result<Self> {
578 let config_path = get_config_path();
579
580 if config_path.exists() {
581 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
582 toml::from_str(&content).context("Failed to parse config file")
583 } else {
584 let config = Config::default();
585 config.save()?;
586 Ok(config)
587 }
588 }
589
590 pub fn save(&self) -> Result<()> {
592 let config_path = get_config_path();
593
594 if let Some(parent) = config_path.parent() {
596 fs::create_dir_all(parent)?;
597 }
598
599 let content = toml::to_string_pretty(self)?;
600 fs::write(&config_path, content)?;
601
602 Ok(())
603 }
604}
605
606pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
608
609pub fn ensure_auth_cookie() -> Result<(String, String)> {
611 let cookie_path = get_auth_cookie_path();
612
613 if cookie_path.exists() {
614 read_auth_cookie()
615 } else {
616 generate_auth_cookie()
617 }
618}
619
620pub fn read_auth_cookie() -> Result<(String, String)> {
622 let cookie_path = get_auth_cookie_path();
623 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
624
625 let parts: Vec<&str> = content.trim().split(':').collect();
626 if parts.len() != 2 {
627 anyhow::bail!("Invalid auth cookie format");
628 }
629
630 Ok((parts[0].to_string(), parts[1].to_string()))
631}
632
633pub fn ensure_keys() -> Result<(Keys, bool)> {
636 let keys_path = get_keys_path();
637
638 if keys_path.exists() {
639 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
640 let entries = hashtree_config::parse_keys_file(&content);
641 let nsec_str = entries
642 .into_iter()
643 .next()
644 .map(|e| e.secret)
645 .context("Keys file is empty")?;
646 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
647 let keys = Keys::new(secret_key);
648 Ok((keys, false))
649 } else {
650 let keys = generate_keys()?;
651 Ok((keys, true))
652 }
653}
654
655pub fn read_keys() -> Result<Keys> {
657 let keys_path = get_keys_path();
658 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
659 let entries = hashtree_config::parse_keys_file(&content);
660 let nsec_str = entries
661 .into_iter()
662 .next()
663 .map(|e| e.secret)
664 .context("Keys file is empty")?;
665 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
666 Ok(Keys::new(secret_key))
667}
668
669pub fn ensure_keys_string() -> Result<(String, bool)> {
672 let keys_path = get_keys_path();
673
674 if keys_path.exists() {
675 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
676 let entries = hashtree_config::parse_keys_file(&content);
677 let nsec_str = entries
678 .into_iter()
679 .next()
680 .map(|e| e.secret)
681 .context("Keys file is empty")?;
682 Ok((nsec_str, false))
683 } else {
684 let keys = generate_keys()?;
685 let nsec = keys
686 .secret_key()
687 .to_bech32()
688 .context("Failed to encode nsec")?;
689 Ok((nsec, true))
690 }
691}
692
693pub fn generate_keys() -> Result<Keys> {
695 let keys_path = get_keys_path();
696
697 if let Some(parent) = keys_path.parent() {
699 fs::create_dir_all(parent)?;
700 }
701
702 let keys = Keys::generate();
704 let nsec = keys
705 .secret_key()
706 .to_bech32()
707 .context("Failed to encode nsec")?;
708
709 fs::write(&keys_path, &nsec)?;
711
712 #[cfg(unix)]
714 {
715 use std::os::unix::fs::PermissionsExt;
716 let perms = fs::Permissions::from_mode(0o600);
717 fs::set_permissions(&keys_path, perms)?;
718 }
719
720 if let Ok(config) = Config::load() {
721 let data_dir = std::path::PathBuf::from(&config.storage.data_dir);
722 let _ = crate::bootstrap::seed_identity_defaults(&data_dir, &config);
723 }
724
725 Ok(keys)
726}
727
728pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
730 keys.public_key().to_bytes()
731}
732
733pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
735 use nostr::PublicKey;
736 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
737 Ok(pk.to_bytes())
738}
739
740pub fn generate_auth_cookie() -> Result<(String, String)> {
742 use rand::Rng;
743
744 let cookie_path = get_auth_cookie_path();
745
746 if let Some(parent) = cookie_path.parent() {
748 fs::create_dir_all(parent)?;
749 }
750
751 let mut rng = rand::thread_rng();
753 let username = format!("htree_{}", rng.gen::<u32>());
754 let password: String = (0..32)
755 .map(|_| {
756 let idx = rng.gen_range(0..62);
757 match idx {
758 0..=25 => (b'a' + idx) as char,
759 26..=51 => (b'A' + (idx - 26)) as char,
760 _ => (b'0' + (idx - 52)) as char,
761 }
762 })
763 .collect();
764
765 let content = format!("{}:{}", username, password);
767 fs::write(&cookie_path, content)?;
768
769 #[cfg(unix)]
771 {
772 use std::os::unix::fs::PermissionsExt;
773 let perms = fs::Permissions::from_mode(0o600);
774 fs::set_permissions(&cookie_path, perms)?;
775 }
776
777 Ok((username, password))
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783 use tempfile::TempDir;
784
785 #[test]
786 fn test_config_default() {
787 let config = Config::default();
788 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
789 assert!(config.server.enable_auth);
790 assert!(config.server.enable_multicast);
791 assert_eq!(config.server.multicast_group, "239.255.42.98");
792 assert_eq!(config.server.multicast_port, 48555);
793 assert_eq!(config.server.max_multicast_peers, 12);
794 assert!(!config.server.enable_wifi_aware);
795 assert_eq!(config.server.max_wifi_aware_peers, 0);
796 assert!(!config.server.enable_bluetooth);
797 assert_eq!(config.server.max_bluetooth_peers, 0);
798 assert_eq!(config.storage.max_size_gb, 10);
799 assert!(config.nostr.enabled);
800 assert!(config
801 .nostr
802 .relays
803 .contains(&"wss://upload.iris.to/nostr".to_string()));
804 assert!(config.blossom.enabled);
805 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
806 assert_eq!(config.nostr.max_write_distance, 3);
807 assert_eq!(config.nostr.db_max_size_gb, 10);
808 assert_eq!(config.nostr.spambox_max_size_gb, 1);
809 assert!(!config.nostr.negentropy_only);
810 assert_eq!(config.nostr.overmute_threshold, 1.0);
811 assert_eq!(config.nostr.mirror_kinds, vec![0, 3]);
812 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
813 assert!(config.nostr.history_sync_on_reconnect);
814 assert!(config.nostr.socialgraph_root.is_none());
815 assert_eq!(
816 config.nostr.bootstrap_follows,
817 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
818 );
819 assert!(!config.server.socialgraph_snapshot_public);
820 assert!(config.cashu.accepted_mints.is_empty());
821 assert!(config.cashu.default_mint.is_none());
822 assert_eq!(config.cashu.quote_payment_offer_sat, 3);
823 assert_eq!(config.cashu.quote_ttl_ms, 1_500);
824 assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
825 assert_eq!(config.cashu.mint_failure_block_threshold, 2);
826 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
827 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
828 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
829 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
830 assert_eq!(config.cashu.payment_default_block_threshold, 0);
831 assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
832 }
833
834 #[test]
835 fn test_nostr_config_deserialize_with_defaults() {
836 let toml_str = r#"
837[nostr]
838relays = ["wss://relay.damus.io"]
839"#;
840 let config: Config = toml::from_str(toml_str).unwrap();
841 assert!(config.nostr.enabled);
842 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
843 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
844 assert_eq!(config.nostr.max_write_distance, 3);
845 assert_eq!(config.nostr.db_max_size_gb, 10);
846 assert_eq!(config.nostr.spambox_max_size_gb, 1);
847 assert!(!config.nostr.negentropy_only);
848 assert_eq!(config.nostr.overmute_threshold, 1.0);
849 assert_eq!(config.nostr.mirror_kinds, vec![0, 3]);
850 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
851 assert!(config.nostr.history_sync_on_reconnect);
852 assert!(config.nostr.socialgraph_root.is_none());
853 assert_eq!(
854 config.nostr.bootstrap_follows,
855 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
856 );
857 }
858
859 #[test]
860 fn test_nostr_config_deserialize_with_socialgraph() {
861 let toml_str = r#"
862[nostr]
863relays = ["wss://relay.damus.io"]
864socialgraph_root = "npub1test"
865bootstrap_follows = []
866social_graph_crawl_depth = 3
867max_write_distance = 5
868negentropy_only = true
869overmute_threshold = 2.5
870mirror_kinds = [0, 10000]
871history_sync_author_chunk_size = 250
872history_sync_on_reconnect = false
873"#;
874 let config: Config = toml::from_str(toml_str).unwrap();
875 assert!(config.nostr.enabled);
876 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
877 assert!(config.nostr.bootstrap_follows.is_empty());
878 assert_eq!(config.nostr.social_graph_crawl_depth, 3);
879 assert_eq!(config.nostr.max_write_distance, 5);
880 assert_eq!(config.nostr.db_max_size_gb, 10);
881 assert_eq!(config.nostr.spambox_max_size_gb, 1);
882 assert!(config.nostr.negentropy_only);
883 assert_eq!(config.nostr.overmute_threshold, 2.5);
884 assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
885 assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
886 assert!(!config.nostr.history_sync_on_reconnect);
887 }
888
889 #[test]
890 fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
891 let toml_str = r#"
892[nostr]
893relays = ["wss://relay.damus.io"]
894crawl_depth = 4
895"#;
896 let config: Config = toml::from_str(toml_str).unwrap();
897 assert_eq!(config.nostr.social_graph_crawl_depth, 4);
898 }
899
900 #[test]
901 fn test_server_config_deserialize_with_multicast() {
902 let toml_str = r#"
903[server]
904enable_multicast = true
905multicast_group = "239.255.42.99"
906multicast_port = 49001
907max_multicast_peers = 12
908enable_wifi_aware = true
909max_wifi_aware_peers = 5
910enable_bluetooth = true
911max_bluetooth_peers = 6
912"#;
913 let config: Config = toml::from_str(toml_str).unwrap();
914 assert!(config.server.enable_multicast);
915 assert_eq!(config.server.multicast_group, "239.255.42.99");
916 assert_eq!(config.server.multicast_port, 49_001);
917 assert_eq!(config.server.max_multicast_peers, 12);
918 assert!(config.server.enable_wifi_aware);
919 assert_eq!(config.server.max_wifi_aware_peers, 5);
920 assert!(config.server.enable_bluetooth);
921 assert_eq!(config.server.max_bluetooth_peers, 6);
922 }
923
924 #[test]
925 fn test_cashu_config_deserialize_with_accepted_mints() {
926 let toml_str = r#"
927[cashu]
928accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
929default_mint = "https://mint1.example"
930quote_payment_offer_sat = 5
931quote_ttl_ms = 2500
932settlement_timeout_ms = 7000
933mint_failure_block_threshold = 3
934peer_suggested_mint_base_cap_sat = 4
935peer_suggested_mint_success_step_sat = 2
936peer_suggested_mint_receipt_step_sat = 3
937peer_suggested_mint_max_cap_sat = 34
938payment_default_block_threshold = 2
939chunk_target_bytes = 65536
940"#;
941 let config: Config = toml::from_str(toml_str).unwrap();
942 assert_eq!(
943 config.cashu.accepted_mints,
944 vec![
945 "https://mint1.example".to_string(),
946 "http://127.0.0.1:3338".to_string()
947 ]
948 );
949 assert_eq!(
950 config.cashu.default_mint,
951 Some("https://mint1.example".to_string())
952 );
953 assert_eq!(config.cashu.quote_payment_offer_sat, 5);
954 assert_eq!(config.cashu.quote_ttl_ms, 2500);
955 assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
956 assert_eq!(config.cashu.mint_failure_block_threshold, 3);
957 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
958 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
959 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
960 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
961 assert_eq!(config.cashu.payment_default_block_threshold, 2);
962 assert_eq!(config.cashu.chunk_target_bytes, 65_536);
963 }
964
965 #[test]
966 fn test_auth_cookie_generation() -> Result<()> {
967 let temp_dir = TempDir::new()?;
968
969 std::env::set_var("HOME", temp_dir.path());
971
972 let (username, password) = generate_auth_cookie()?;
973
974 assert!(username.starts_with("htree_"));
975 assert_eq!(password.len(), 32);
976
977 let cookie_path = get_auth_cookie_path();
979 assert!(cookie_path.exists());
980
981 let (u2, p2) = read_auth_cookie()?;
983 assert_eq!(username, u2);
984 assert_eq!(password, p2);
985
986 Ok(())
987 }
988
989 #[test]
990 fn test_blossom_read_servers_include_write_only_servers_as_fresh_fallbacks() {
991 let mut config = BlossomConfig::default();
992 config.servers = vec!["https://legacy.server".to_string()];
993
994 let read = config.all_read_servers();
995 assert!(read.contains(&"https://legacy.server".to_string()));
996 assert!(read.contains(&"https://cdn.iris.to".to_string()));
997 assert!(read.contains(&"https://blossom.primal.net".to_string()));
998 assert!(read.contains(&"https://upload.iris.to".to_string()));
999
1000 let write = config.all_write_servers();
1001 assert!(write.contains(&"https://legacy.server".to_string()));
1002 assert!(write.contains(&"https://upload.iris.to".to_string()));
1003 }
1004
1005 #[test]
1006 fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1007 let config = BlossomConfig {
1008 enabled: true,
1009 servers: Vec::new(),
1010 read_servers: Vec::new(),
1011 write_servers: Vec::new(),
1012 max_upload_mb: default_max_upload_mb(),
1013 };
1014
1015 let read = config.all_read_servers();
1016 let mut expected = default_read_servers();
1017 expected.extend(default_write_servers());
1018 expected.sort();
1019 expected.dedup();
1020 assert_eq!(read, expected);
1021
1022 let write = config.all_write_servers();
1023 assert_eq!(write, default_write_servers());
1024 }
1025
1026 #[test]
1027 fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1028 let nostr = NostrConfig {
1029 enabled: false,
1030 relays: vec!["wss://relay.example".to_string()],
1031 ..NostrConfig::default()
1032 };
1033 assert!(nostr.active_relays().is_empty());
1034
1035 let blossom = BlossomConfig {
1036 enabled: false,
1037 servers: vec!["https://legacy.server".to_string()],
1038 read_servers: vec!["https://read.example".to_string()],
1039 write_servers: vec!["https://write.example".to_string()],
1040 max_upload_mb: default_max_upload_mb(),
1041 };
1042 assert!(blossom.all_read_servers().is_empty());
1043 assert!(blossom.all_write_servers().is_empty());
1044 }
1045}