Skip to main content

agent_first_pay/
config.rs

1use crate::types::*;
2use agent_first_data::cli_parse_log_filters;
3use std::path::Path;
4
5pub const VERSION: &str = env!("CARGO_PKG_VERSION");
6
7pub fn enabled_features() -> Vec<&'static str> {
8    let features: &[Option<&str>] = &[
9        #[cfg(feature = "redb")]
10        Some("redb"),
11        #[cfg(feature = "postgres")]
12        Some("postgres"),
13        #[cfg(feature = "cashu")]
14        Some("cashu"),
15        #[cfg(feature = "ln-nwc")]
16        Some("ln-nwc"),
17        #[cfg(feature = "ln-phoenixd")]
18        Some("ln-phoenixd"),
19        #[cfg(feature = "ln-lnbits")]
20        Some("ln-lnbits"),
21        #[cfg(feature = "sol")]
22        Some("sol"),
23        #[cfg(feature = "evm")]
24        Some("evm"),
25        #[cfg(feature = "btc-esplora")]
26        Some("btc-esplora"),
27        #[cfg(feature = "btc-core")]
28        Some("btc-core"),
29        #[cfg(feature = "btc-electrum")]
30        Some("btc-electrum"),
31        #[cfg(feature = "interactive")]
32        Some("interactive"),
33        #[cfg(feature = "rest")]
34        Some("rest"),
35    ];
36    features.iter().copied().flatten().collect()
37}
38
39/// Single source of truth for startup log — always includes env.features.
40pub fn build_startup_log(
41    argv: Option<Vec<String>>,
42    config: Option<&RuntimeConfig>,
43    args: serde_json::Value,
44) -> Output {
45    Output::Log {
46        event: "startup".to_string(),
47        request_id: None,
48        version: Some(VERSION.to_string()),
49        argv,
50        config: config.map(|c| serde_json::to_value(c).unwrap_or(serde_json::Value::Null)),
51        args: Some(args),
52        env: Some(serde_json::json!({
53            "features": enabled_features(),
54        })),
55        trace: Trace::from_duration(0),
56    }
57}
58
59/// Decide whether startup log should be emitted for this process.
60/// Startup is emitted when explicit startup logging is requested or any log filter is set.
61pub fn should_emit_startup_log(log_filters: &[String], startup_requested: bool) -> bool {
62    startup_requested || !log_filters.is_empty()
63}
64
65/// Unified startup log builder + gate used by all runtime modes.
66pub fn maybe_startup_log(
67    log_filters: &[String],
68    startup_requested: bool,
69    argv: Option<Vec<String>>,
70    config: Option<&RuntimeConfig>,
71    args: serde_json::Value,
72) -> Option<Output> {
73    if !should_emit_startup_log(log_filters, startup_requested) {
74        return None;
75    }
76    Some(build_startup_log(argv, config, args))
77}
78
79impl RuntimeConfig {
80    /// Load config from `{data_dir}/config.toml`. Falls back to defaults if file missing.
81    pub fn load_from_dir(data_dir: &str) -> Result<Self, String> {
82        let path = Path::new(data_dir).join("config.toml");
83        if !path.exists() {
84            return Ok(Self {
85                data_dir: data_dir.to_string(),
86                ..Self::default()
87            });
88        }
89        let contents =
90            std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?;
91        let mut cfg: Self =
92            toml::from_str(&contents).map_err(|e| format!("parse {}: {e}", path.display()))?;
93        // Ensure data_dir reflects the actual directory (config file may omit it)
94        cfg.data_dir = data_dir.to_string();
95        Ok(cfg)
96    }
97
98    #[allow(dead_code)]
99    pub fn apply_update(&mut self, patch: ConfigPatch) {
100        if let Some(v) = patch.data_dir {
101            self.data_dir = v;
102        }
103        if let Some(v) = patch.limits {
104            self.limits = v;
105        }
106        if let Some(v) = patch.log {
107            self.log = cli_parse_log_filters(&v);
108        }
109        if let Some(rpc_nodes) = patch.afpay_rpc {
110            for (name, cfg) in rpc_nodes {
111                self.afpay_rpc.insert(name, cfg);
112            }
113        }
114        if let Some(providers) = patch.providers {
115            for (network, rpc_name) in providers {
116                self.providers.insert(network, rpc_name);
117            }
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn maybe_startup_log_disabled_without_filters_or_request() {
128        let out = maybe_startup_log(&[], false, None, None, serde_json::json!({"mode": "test"}));
129        assert!(out.is_none());
130    }
131
132    #[test]
133    fn maybe_startup_log_enabled_with_filters() {
134        let filters = vec!["cashu".to_string()];
135        let out = maybe_startup_log(
136            &filters,
137            false,
138            None,
139            None,
140            serde_json::json!({"mode": "test"}),
141        );
142        assert!(out.is_some());
143    }
144
145    #[test]
146    fn maybe_startup_log_enabled_with_explicit_request() {
147        let out = maybe_startup_log(&[], true, None, None, serde_json::json!({"mode": "test"}));
148        assert!(out.is_some());
149    }
150}