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