Skip to main content

hashtree_cli/
config.rs

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    /// Port for the built-in STUN server (0 = disabled)
30    #[serde(default = "default_stun_port")]
31    pub stun_port: u16,
32    /// Enable WebRTC P2P connections
33    #[serde(default = "default_enable_webrtc")]
34    pub enable_webrtc: bool,
35    /// Allow anyone with valid Nostr auth to write (default: true)
36    /// When false, only social graph members can write
37    #[serde(default = "default_public_writes")]
38    pub public_writes: bool,
39    /// Allow public access to social graph snapshot endpoint (default: false)
40    #[serde(default = "default_socialgraph_snapshot_public")]
41    pub socialgraph_snapshot_public: bool,
42}
43
44fn default_public_writes() -> bool {
45    true
46}
47
48fn default_socialgraph_snapshot_public() -> bool {
49    false
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct StorageConfig {
54    #[serde(default = "default_data_dir")]
55    pub data_dir: String,
56    #[serde(default = "default_max_size_gb")]
57    pub max_size_gb: u64,
58    /// Optional S3/R2 backend for blob storage
59    #[serde(default)]
60    pub s3: Option<S3Config>,
61}
62
63/// S3-compatible storage configuration (works with AWS S3, Cloudflare R2, MinIO, etc.)
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct S3Config {
66    /// S3 endpoint URL (e.g., "https://<account_id>.r2.cloudflarestorage.com" for R2)
67    pub endpoint: String,
68    /// S3 bucket name
69    pub bucket: String,
70    /// Optional key prefix for all blobs (e.g., "blobs/")
71    #[serde(default)]
72    pub prefix: Option<String>,
73    /// AWS region (use "auto" for R2)
74    #[serde(default = "default_s3_region")]
75    pub region: String,
76    /// Access key ID (can also be set via AWS_ACCESS_KEY_ID env var)
77    #[serde(default)]
78    pub access_key: Option<String>,
79    /// Secret access key (can also be set via AWS_SECRET_ACCESS_KEY env var)
80    #[serde(default)]
81    pub secret_key: Option<String>,
82    /// Public URL for serving blobs (optional, for generating public URLs)
83    #[serde(default)]
84    pub public_url: Option<String>,
85}
86
87fn default_s3_region() -> String {
88    "auto".to_string()
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct NostrConfig {
93    #[serde(default = "default_relays")]
94    pub relays: Vec<String>,
95    /// List of npubs allowed to write (blossom uploads). If empty, uses public_writes setting.
96    #[serde(default)]
97    pub allowed_npubs: Vec<String>,
98    /// Social graph root pubkey (npub). Defaults to own key if not set.
99    #[serde(default)]
100    pub socialgraph_root: Option<String>,
101    /// How many hops to crawl the follow graph (default: 2)
102    #[serde(default = "default_crawl_depth")]
103    pub crawl_depth: u32,
104    /// Max follow distance for write access (default: 3)
105    #[serde(default = "default_max_write_distance")]
106    pub max_write_distance: u32,
107    /// Max size for the trusted social graph store in GB (default: 10)
108    #[serde(default = "default_nostr_db_max_size_gb")]
109    pub db_max_size_gb: u64,
110    /// Max size for the social graph spambox in GB (default: 1)
111    /// Set to 0 for memory-only spambox (no on-disk DB)
112    #[serde(default = "default_nostr_spambox_max_size_gb")]
113    pub spambox_max_size_gb: u64,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct BlossomConfig {
118    /// File servers for push/pull (legacy, both read and write)
119    #[serde(default)]
120    pub servers: Vec<String>,
121    /// Read-only file servers (fallback for fetching content)
122    #[serde(default = "default_read_servers")]
123    pub read_servers: Vec<String>,
124    /// Write-enabled file servers (for uploading)
125    #[serde(default = "default_write_servers")]
126    pub write_servers: Vec<String>,
127    /// Maximum upload size in MB (default: 5)
128    #[serde(default = "default_max_upload_mb")]
129    pub max_upload_mb: u64,
130}
131
132// Keep in sync with hashtree-config/src/lib.rs
133fn default_read_servers() -> Vec<String> {
134    vec![
135        "https://cdn.iris.to".to_string(),
136        "https://hashtree.iris.to".to_string(),
137    ]
138}
139
140fn default_write_servers() -> Vec<String> {
141    vec!["https://upload.iris.to".to_string()]
142}
143
144fn default_max_upload_mb() -> u64 {
145    5
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct SyncConfig {
150    /// Enable background sync (auto-pull trees)
151    #[serde(default = "default_sync_enabled")]
152    pub enabled: bool,
153    /// Sync own trees (subscribed via Nostr)
154    #[serde(default = "default_sync_own")]
155    pub sync_own: bool,
156    /// Sync followed users' public trees
157    #[serde(default = "default_sync_followed")]
158    pub sync_followed: bool,
159    /// Max concurrent sync tasks
160    #[serde(default = "default_max_concurrent")]
161    pub max_concurrent: usize,
162    /// WebRTC request timeout in milliseconds
163    #[serde(default = "default_webrtc_timeout_ms")]
164    pub webrtc_timeout_ms: u64,
165    /// Blossom request timeout in milliseconds
166    #[serde(default = "default_blossom_timeout_ms")]
167    pub blossom_timeout_ms: u64,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct CashuConfig {
172    /// Cashu mint base URLs we accept for bandwidth incentives.
173    #[serde(default)]
174    pub accepted_mints: Vec<String>,
175    /// Default mint to use for wallet operations.
176    #[serde(default)]
177    pub default_mint: Option<String>,
178    /// Default post-delivery payment offer for quoted retrievals.
179    #[serde(default = "default_cashu_quote_payment_offer_sat")]
180    pub quote_payment_offer_sat: u64,
181    /// Quote validity window in milliseconds.
182    #[serde(default = "default_cashu_quote_ttl_ms")]
183    pub quote_ttl_ms: u32,
184    /// Maximum time to wait for post-delivery settlement before recording a default.
185    #[serde(default = "default_cashu_settlement_timeout_ms")]
186    pub settlement_timeout_ms: u64,
187    /// Block mints whose failed redemptions keep outnumbering successful redemptions.
188    #[serde(default = "default_cashu_mint_failure_block_threshold")]
189    pub mint_failure_block_threshold: u64,
190    /// Base cap for trying a peer-suggested mint we do not already trust.
191    #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
192    pub peer_suggested_mint_base_cap_sat: u64,
193    /// Additional cap granted per successful delivery from that peer.
194    #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
195    pub peer_suggested_mint_success_step_sat: u64,
196    /// Additional cap granted per settled payment received from that peer.
197    #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
198    pub peer_suggested_mint_receipt_step_sat: u64,
199    /// Hard ceiling for untrusted peer-suggested mint exposure.
200    #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
201    pub peer_suggested_mint_max_cap_sat: u64,
202    /// Block serving peers whose unpaid defaults reach this threshold.
203    #[serde(default)]
204    pub payment_default_block_threshold: u64,
205    /// Target chunk size for quoted paid delivery.
206    #[serde(default = "default_cashu_chunk_target_bytes")]
207    pub chunk_target_bytes: usize,
208}
209
210impl Default for CashuConfig {
211    fn default() -> Self {
212        Self {
213            accepted_mints: Vec::new(),
214            default_mint: None,
215            quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
216            quote_ttl_ms: default_cashu_quote_ttl_ms(),
217            settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
218            mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
219            peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
220            peer_suggested_mint_success_step_sat:
221                default_cashu_peer_suggested_mint_success_step_sat(),
222            peer_suggested_mint_receipt_step_sat:
223                default_cashu_peer_suggested_mint_receipt_step_sat(),
224            peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
225            payment_default_block_threshold: 0,
226            chunk_target_bytes: default_cashu_chunk_target_bytes(),
227        }
228    }
229}
230
231fn default_cashu_quote_payment_offer_sat() -> u64 {
232    3
233}
234
235fn default_cashu_quote_ttl_ms() -> u32 {
236    1_500
237}
238
239fn default_cashu_settlement_timeout_ms() -> u64 {
240    5_000
241}
242
243fn default_cashu_mint_failure_block_threshold() -> u64 {
244    2
245}
246
247fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
248    3
249}
250
251fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
252    1
253}
254
255fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
256    2
257}
258
259fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
260    21
261}
262
263fn default_cashu_chunk_target_bytes() -> usize {
264    32 * 1024
265}
266
267fn default_sync_enabled() -> bool {
268    true
269}
270
271fn default_sync_own() -> bool {
272    true
273}
274
275fn default_sync_followed() -> bool {
276    true
277}
278
279fn default_max_concurrent() -> usize {
280    3
281}
282
283fn default_webrtc_timeout_ms() -> u64 {
284    2000
285}
286
287fn default_blossom_timeout_ms() -> u64 {
288    10000
289}
290
291fn default_crawl_depth() -> u32 {
292    2
293}
294
295fn default_max_write_distance() -> u32 {
296    3
297}
298
299fn default_nostr_db_max_size_gb() -> u64 {
300    10
301}
302
303fn default_nostr_spambox_max_size_gb() -> u64 {
304    1
305}
306
307fn default_relays() -> Vec<String> {
308    vec![
309        "wss://relay.damus.io".to_string(),
310        "wss://relay.snort.social".to_string(),
311        "wss://nos.lol".to_string(),
312        "wss://temp.iris.to".to_string(),
313    ]
314}
315
316fn default_bind_address() -> String {
317    "127.0.0.1:8080".to_string()
318}
319
320fn default_enable_auth() -> bool {
321    true
322}
323
324fn default_stun_port() -> u16 {
325    3478 // Standard STUN port (RFC 5389)
326}
327
328fn default_enable_webrtc() -> bool {
329    true
330}
331
332fn default_data_dir() -> String {
333    hashtree_config::get_hashtree_dir()
334        .join("data")
335        .to_string_lossy()
336        .to_string()
337}
338
339fn default_max_size_gb() -> u64 {
340    10
341}
342
343impl Default for ServerConfig {
344    fn default() -> Self {
345        Self {
346            bind_address: default_bind_address(),
347            enable_auth: default_enable_auth(),
348            stun_port: default_stun_port(),
349            enable_webrtc: default_enable_webrtc(),
350            public_writes: default_public_writes(),
351            socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
352        }
353    }
354}
355
356impl Default for StorageConfig {
357    fn default() -> Self {
358        Self {
359            data_dir: default_data_dir(),
360            max_size_gb: default_max_size_gb(),
361            s3: None,
362        }
363    }
364}
365
366impl Default for NostrConfig {
367    fn default() -> Self {
368        Self {
369            relays: default_relays(),
370            allowed_npubs: Vec::new(),
371            socialgraph_root: None,
372            crawl_depth: default_crawl_depth(),
373            max_write_distance: default_max_write_distance(),
374            db_max_size_gb: default_nostr_db_max_size_gb(),
375            spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
376        }
377    }
378}
379
380impl Default for BlossomConfig {
381    fn default() -> Self {
382        Self {
383            servers: Vec::new(),
384            read_servers: default_read_servers(),
385            write_servers: default_write_servers(),
386            max_upload_mb: default_max_upload_mb(),
387        }
388    }
389}
390
391impl Default for SyncConfig {
392    fn default() -> Self {
393        Self {
394            enabled: default_sync_enabled(),
395            sync_own: default_sync_own(),
396            sync_followed: default_sync_followed(),
397            max_concurrent: default_max_concurrent(),
398            webrtc_timeout_ms: default_webrtc_timeout_ms(),
399            blossom_timeout_ms: default_blossom_timeout_ms(),
400        }
401    }
402}
403
404impl Config {
405    /// Load config from file, or create default if doesn't exist
406    pub fn load() -> Result<Self> {
407        let config_path = get_config_path();
408
409        if config_path.exists() {
410            let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
411            toml::from_str(&content).context("Failed to parse config file")
412        } else {
413            let config = Config::default();
414            config.save()?;
415            Ok(config)
416        }
417    }
418
419    /// Save config to file
420    pub fn save(&self) -> Result<()> {
421        let config_path = get_config_path();
422
423        // Ensure parent directory exists
424        if let Some(parent) = config_path.parent() {
425            fs::create_dir_all(parent)?;
426        }
427
428        let content = toml::to_string_pretty(self)?;
429        fs::write(&config_path, content)?;
430
431        Ok(())
432    }
433}
434
435// Re-export path functions from hashtree_config
436pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
437
438/// Generate and save auth cookie if it doesn't exist
439pub fn ensure_auth_cookie() -> Result<(String, String)> {
440    let cookie_path = get_auth_cookie_path();
441
442    if cookie_path.exists() {
443        read_auth_cookie()
444    } else {
445        generate_auth_cookie()
446    }
447}
448
449/// Read existing auth cookie
450pub fn read_auth_cookie() -> Result<(String, String)> {
451    let cookie_path = get_auth_cookie_path();
452    let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
453
454    let parts: Vec<&str> = content.trim().split(':').collect();
455    if parts.len() != 2 {
456        anyhow::bail!("Invalid auth cookie format");
457    }
458
459    Ok((parts[0].to_string(), parts[1].to_string()))
460}
461
462/// Ensure keys file exists, generating one if not present
463/// Returns (Keys, was_generated)
464pub fn ensure_keys() -> Result<(Keys, bool)> {
465    let keys_path = get_keys_path();
466
467    if keys_path.exists() {
468        let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
469        let entries = hashtree_config::parse_keys_file(&content);
470        let nsec_str = entries
471            .into_iter()
472            .next()
473            .map(|e| e.secret)
474            .context("Keys file is empty")?;
475        let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
476        let keys = Keys::new(secret_key);
477        Ok((keys, false))
478    } else {
479        let keys = generate_keys()?;
480        Ok((keys, true))
481    }
482}
483
484/// Read existing keys
485pub fn read_keys() -> Result<Keys> {
486    let keys_path = get_keys_path();
487    let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
488    let entries = hashtree_config::parse_keys_file(&content);
489    let nsec_str = entries
490        .into_iter()
491        .next()
492        .map(|e| e.secret)
493        .context("Keys file is empty")?;
494    let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
495    Ok(Keys::new(secret_key))
496}
497
498/// Get nsec string, ensuring keys file exists (generate if needed)
499/// Returns (nsec_string, was_generated)
500pub fn ensure_keys_string() -> Result<(String, bool)> {
501    let keys_path = get_keys_path();
502
503    if keys_path.exists() {
504        let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
505        let entries = hashtree_config::parse_keys_file(&content);
506        let nsec_str = entries
507            .into_iter()
508            .next()
509            .map(|e| e.secret)
510            .context("Keys file is empty")?;
511        Ok((nsec_str, false))
512    } else {
513        let keys = generate_keys()?;
514        let nsec = keys
515            .secret_key()
516            .to_bech32()
517            .context("Failed to encode nsec")?;
518        Ok((nsec, true))
519    }
520}
521
522/// Generate new keys and save to file
523pub fn generate_keys() -> Result<Keys> {
524    let keys_path = get_keys_path();
525
526    // Ensure parent directory exists
527    if let Some(parent) = keys_path.parent() {
528        fs::create_dir_all(parent)?;
529    }
530
531    // Generate new keys
532    let keys = Keys::generate();
533    let nsec = keys
534        .secret_key()
535        .to_bech32()
536        .context("Failed to encode nsec")?;
537
538    // Save to file
539    fs::write(&keys_path, &nsec)?;
540
541    // Set permissions to 0600 (owner read/write only)
542    #[cfg(unix)]
543    {
544        use std::os::unix::fs::PermissionsExt;
545        let perms = fs::Permissions::from_mode(0o600);
546        fs::set_permissions(&keys_path, perms)?;
547    }
548
549    Ok(keys)
550}
551
552/// Get 32-byte pubkey bytes from Keys.
553pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
554    keys.public_key().to_bytes()
555}
556
557/// Parse npub to 32-byte pubkey
558pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
559    use nostr::PublicKey;
560    let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
561    Ok(pk.to_bytes())
562}
563
564/// Generate new random auth cookie
565pub fn generate_auth_cookie() -> Result<(String, String)> {
566    use rand::Rng;
567
568    let cookie_path = get_auth_cookie_path();
569
570    // Ensure parent directory exists
571    if let Some(parent) = cookie_path.parent() {
572        fs::create_dir_all(parent)?;
573    }
574
575    // Generate random credentials
576    let mut rng = rand::thread_rng();
577    let username = format!("htree_{}", rng.gen::<u32>());
578    let password: String = (0..32)
579        .map(|_| {
580            let idx = rng.gen_range(0..62);
581            match idx {
582                0..=25 => (b'a' + idx) as char,
583                26..=51 => (b'A' + (idx - 26)) as char,
584                _ => (b'0' + (idx - 52)) as char,
585            }
586        })
587        .collect();
588
589    // Save to file
590    let content = format!("{}:{}", username, password);
591    fs::write(&cookie_path, content)?;
592
593    // Set permissions to 0600 (owner read/write only)
594    #[cfg(unix)]
595    {
596        use std::os::unix::fs::PermissionsExt;
597        let perms = fs::Permissions::from_mode(0o600);
598        fs::set_permissions(&cookie_path, perms)?;
599    }
600
601    Ok((username, password))
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use tempfile::TempDir;
608
609    #[test]
610    fn test_config_default() {
611        let config = Config::default();
612        assert_eq!(config.server.bind_address, "127.0.0.1:8080");
613        assert!(config.server.enable_auth);
614        assert_eq!(config.storage.max_size_gb, 10);
615        assert_eq!(config.nostr.crawl_depth, 2);
616        assert_eq!(config.nostr.max_write_distance, 3);
617        assert_eq!(config.nostr.db_max_size_gb, 10);
618        assert_eq!(config.nostr.spambox_max_size_gb, 1);
619        assert!(config.nostr.socialgraph_root.is_none());
620        assert!(!config.server.socialgraph_snapshot_public);
621        assert!(config.cashu.accepted_mints.is_empty());
622        assert!(config.cashu.default_mint.is_none());
623        assert_eq!(config.cashu.quote_payment_offer_sat, 3);
624        assert_eq!(config.cashu.quote_ttl_ms, 1_500);
625        assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
626        assert_eq!(config.cashu.mint_failure_block_threshold, 2);
627        assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
628        assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
629        assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
630        assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
631        assert_eq!(config.cashu.payment_default_block_threshold, 0);
632        assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
633    }
634
635    #[test]
636    fn test_nostr_config_deserialize_with_defaults() {
637        let toml_str = r#"
638[nostr]
639relays = ["wss://relay.damus.io"]
640"#;
641        let config: Config = toml::from_str(toml_str).unwrap();
642        assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
643        assert_eq!(config.nostr.crawl_depth, 2);
644        assert_eq!(config.nostr.max_write_distance, 3);
645        assert_eq!(config.nostr.db_max_size_gb, 10);
646        assert_eq!(config.nostr.spambox_max_size_gb, 1);
647        assert!(config.nostr.socialgraph_root.is_none());
648    }
649
650    #[test]
651    fn test_nostr_config_deserialize_with_socialgraph() {
652        let toml_str = r#"
653[nostr]
654relays = ["wss://relay.damus.io"]
655socialgraph_root = "npub1test"
656crawl_depth = 3
657max_write_distance = 5
658"#;
659        let config: Config = toml::from_str(toml_str).unwrap();
660        assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
661        assert_eq!(config.nostr.crawl_depth, 3);
662        assert_eq!(config.nostr.max_write_distance, 5);
663        assert_eq!(config.nostr.db_max_size_gb, 10);
664        assert_eq!(config.nostr.spambox_max_size_gb, 1);
665    }
666
667    #[test]
668    fn test_cashu_config_deserialize_with_accepted_mints() {
669        let toml_str = r#"
670[cashu]
671accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
672default_mint = "https://mint1.example"
673quote_payment_offer_sat = 5
674quote_ttl_ms = 2500
675settlement_timeout_ms = 7000
676mint_failure_block_threshold = 3
677peer_suggested_mint_base_cap_sat = 4
678peer_suggested_mint_success_step_sat = 2
679peer_suggested_mint_receipt_step_sat = 3
680peer_suggested_mint_max_cap_sat = 34
681payment_default_block_threshold = 2
682chunk_target_bytes = 65536
683"#;
684        let config: Config = toml::from_str(toml_str).unwrap();
685        assert_eq!(
686            config.cashu.accepted_mints,
687            vec![
688                "https://mint1.example".to_string(),
689                "http://127.0.0.1:3338".to_string()
690            ]
691        );
692        assert_eq!(
693            config.cashu.default_mint,
694            Some("https://mint1.example".to_string())
695        );
696        assert_eq!(config.cashu.quote_payment_offer_sat, 5);
697        assert_eq!(config.cashu.quote_ttl_ms, 2500);
698        assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
699        assert_eq!(config.cashu.mint_failure_block_threshold, 3);
700        assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
701        assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
702        assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
703        assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
704        assert_eq!(config.cashu.payment_default_block_threshold, 2);
705        assert_eq!(config.cashu.chunk_target_bytes, 65_536);
706    }
707
708    #[test]
709    fn test_auth_cookie_generation() -> Result<()> {
710        let temp_dir = TempDir::new()?;
711
712        // Mock the cookie path
713        std::env::set_var("HOME", temp_dir.path());
714
715        let (username, password) = generate_auth_cookie()?;
716
717        assert!(username.starts_with("htree_"));
718        assert_eq!(password.len(), 32);
719
720        // Verify cookie file exists
721        let cookie_path = get_auth_cookie_path();
722        assert!(cookie_path.exists());
723
724        // Verify reading works
725        let (u2, p2) = read_auth_cookie()?;
726        assert_eq!(username, u2);
727        assert_eq!(password, p2);
728
729        Ok(())
730    }
731}