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