Skip to main content

qualia_client_core/api/
qapp_vault.rs

1//! QApp vault, config, wallet status
2
3#![allow(non_snake_case)]
4
5use super::*;
6
7use crate::qapp_paths::qapps_dir;
8use crate::qapp_registry;
9use crate::state::*;
10use serde::Serialize;
11use std::path::PathBuf;
12use std::sync::atomic::Ordering;
13use sysinfo::Disks;
14
15pub fn list_installed_qapps() -> Vec<String> {
16    let state = crate::state::APP_STATE.get().unwrap();
17    let data_dir = state.config.lock().unwrap().storage_path.clone();
18    let qapps_dir = qapps_dir(&data_dir);
19    let mut qapps = Vec::new();
20    if let Ok(entries) = std::fs::read_dir(qapps_dir) {
21        for entry in entries.filter_map(Result::ok) {
22            if entry.path().is_dir() {
23                qapps.push(entry.file_name().to_string_lossy().to_string());
24            }
25        }
26    }
27    qapps
28}
29
30pub fn mcp_list_qapps() -> Result<String, String> {
31    crate::qapp_mcp::list_qapp_catalogue_json()
32}
33
34pub fn mcp_get_qapp_manifest(qapp_name: String) -> Result<String, String> {
35    crate::qapp_mcp::get_qapp_manifest_json(&qapp_name)
36}
37
38pub fn mcp_inspect_qapp_readiness(qapp_name: String) -> Result<String, String> {
39    crate::qapp_mcp::inspect_qapp_readiness_json(&qapp_name)
40}
41
42pub fn mcp_list_qapp_updates() -> Result<String, String> {
43    crate::qapp_mcp::list_qapp_updates_json()
44}
45
46pub fn mcp_describe_qapp_surface_schema() -> Result<String, String> {
47    crate::qapp_mcp::describe_qapp_surface_schema_json()
48}
49
50pub fn generate_qapp_credential(qapp_name: String) -> String {
51    format!("did:qualia:qapp:{}:signed_vc", qapp_name)
52}
53
54pub fn verify_and_install_qapp(target_path: String) -> Result<String, String> {
55    let state = crate::state::APP_STATE.get().unwrap();
56    let storage = state.config.lock().unwrap().storage_path.clone();
57    let storage_path = std::path::PathBuf::from(&storage);
58
59    if let Ok(port) = target_path.parse::<u16>() {
60        return Err(format!(
61            "Dev proxy port registration ({port}) requires a package directory path, not a bare port"
62        ));
63    }
64
65    let source_dir = std::path::PathBuf::from(&target_path);
66    if !source_dir.is_dir() {
67        return Err(format!("Qapp source directory not found: {target_path}"));
68    }
69
70    let entry = crate::qapp_install::install_package_atomic(
71        &storage_path,
72        &source_dir,
73        crate::qapp_install::InstallPolicy::Development,
74        None,
75    )
76    .map_err(|e| e.to_string())?;
77
78    let package_dir =
79        crate::qapp_paths::resolve_active_package_dir(&storage_path, &entry.package_id);
80    let manifest = load_qapp_package_from_dir(&package_dir)?;
81    let qapp_did = format!(
82        "did:qualia:qapp:{}",
83        manifest.name.to_lowercase().replace(" ", "-")
84    );
85
86    let registered_qapp = qapp_registry::RegisteredQapp {
87        did: qapp_did.clone(),
88        manifest: manifest.clone(),
89        target: qapp_registry::QappTarget::IsolatedVault(entry.package_id.clone()),
90    };
91
92    let qapp_id_hash = crate::qapp_manifest::install_qapp_capabilities(&manifest)
93        .map_err(|e| format!("Qapp capability compile failed: {e:?}"))?;
94
95    {
96        let mut installed = state.installed_qapps.lock().unwrap();
97        installed.retain(|q| q.manifest.name != entry.package_id && q.did != qapp_did);
98        installed.push(registered_qapp);
99    }
100    save_directory_state();
101
102    Ok(format!(
103        "{qapp_did} v{} hash={} content={}",
104        entry.active_version, qapp_id_hash, entry.content_hash
105    ))
106}
107
108#[derive(Serialize)]
109pub struct WalletStatus {
110    /// XEC balance in satoshis (live from Chronik when identity is set).
111    pub xec_sats: i64,
112    /// Total ILP micro-cents dispatched (from persistent ledger).
113    pub ilp_dispatched_microcents: u64,
114    /// Whether the Nym mixnet relay is active.
115    pub nym_connected: bool,
116    /// Sync status: "synced" | "offline" | "no_identity".
117    pub sync_status: String,
118}
119
120pub fn get_wallet_status() -> WalletStatus {
121    let nym_connected = crate::state::APP_STATE
122        .get()
123        .map(|s| s.nym_relay_active.load(Ordering::Relaxed))
124        .unwrap_or(false);
125
126    let state = crate::state::APP_STATE.get();
127    let storage_path = state
128        .map(|s| s.config.lock().unwrap().storage_path.clone())
129        .unwrap_or_default();
130
131    // Read ILP dispatched total from persistent ledger
132    let ilp_dispatched =
133        crate::wallet::ledger::total_ilp_sent_micro_cents(&std::path::Path::new(&storage_path));
134
135    // Query live XEC balance from Chronik if identity is set
136    let id = read_identity();
137    let (xec_sats, sync_status) = match id
138        .as_ref()
139        .and_then(|v| v.get("ecash_hash160"))
140        .and_then(|v| v.as_str())
141    {
142        Some(hash160) => {
143            let client = crate::wallet::chronik::ChronikClient::new("https://chronik.be.cash");
144            match client.fetch_utxos_p2pkh(hash160) {
145                Ok(utxos) => {
146                    let sats: i64 = utxos
147                        .iter()
148                        .filter(|u| u.slp_meta.is_none())
149                        .map(|u| u.value)
150                        .sum();
151                    (sats, "synced".to_string())
152                }
153                Err(_) => (0, "offline".to_string()),
154            }
155        }
156        None => (0, "no_identity".to_string()),
157    };
158
159    WalletStatus {
160        xec_sats,
161        ilp_dispatched_microcents: ilp_dispatched,
162        nym_connected,
163        sync_status,
164    }
165}
166
167pub fn nym_mixnet_opted_in() -> bool {
168    crate::state::APP_STATE
169        .get()
170        .map(|s| s.nym_relay_active.load(Ordering::Relaxed))
171        .unwrap_or(false)
172}
173
174pub fn get_config() -> AgentConfig {
175    let state = crate::state::APP_STATE.get().unwrap();
176    state.config.lock().unwrap().clone()
177}
178
179pub fn save_config(new_config: AgentConfig) -> Result<(), String> {
180    let state = crate::state::APP_STATE.get().unwrap();
181    let disks = Disks::new_with_refreshed_list();
182    let path = PathBuf::from(&new_config.storage_path);
183    let mut available = u64::MAX;
184    for disk in disks.list() {
185        if path.starts_with(disk.mount_point()) {
186            available = disk.available_space();
187            break;
188        }
189    }
190    let margin: u64 = 15 * 1024 * 1024 * 1024;
191    let requested = new_config.storage_quota_gb * 1024 * 1024 * 1024;
192    if available.saturating_sub(requested) < margin {
193        return Err(
194            "OS_SAFETY_VIOLATION: Would leave the host OS with less than the 15 GB safety margin."
195                .to_string(),
196        );
197    }
198    // Persist config to disk
199    let meta = app_meta_dir();
200    std::fs::create_dir_all(&meta).map_err(|e| e.to_string())?;
201    let json = serde_json::to_string_pretty(&new_config).map_err(|e| e.to_string())?;
202    std::fs::write(config_file_path(), json).map_err(|e| e.to_string())?;
203    // Ensure data directories exist under the new path
204    init_data_directories(&new_config.storage_path);
205    // Mirror inference_backend string into structured settings (Local/Remote/Hybrid/Ollama).
206    let mut ib = crate::inference_backend::load_inference_backend_settings();
207    ib.apply_agent_config_backend_string(&new_config.inference_backend);
208    let _ = crate::inference_backend::save_inference_backend_settings(&ib);
209    *state.config.lock().unwrap() = new_config;
210    Ok(())
211}