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