Skip to main content

cw_orch_daemon/
state.rs

1use super::error::DaemonError;
2use crate::{
3    env::{default_state_folder, DaemonEnvVars},
4    json_lock::{patch_state_if_old, JsonLockedState},
5    networks::ChainKind,
6};
7
8use cosmwasm_std::Addr;
9use cw_orch_core::{
10    environment::{ChainInfoOwned, CwEnv, Environment, StateInterface},
11    log::local_target,
12    CwEnvError,
13};
14use once_cell::sync::Lazy;
15use serde::Serialize;
16use serde_json::{json, Value};
17use std::{
18    collections::{HashMap, HashSet},
19    path::Path,
20    sync::{Arc, Mutex},
21};
22
23/// Global state to track which files are already open by other daemons from other threads
24/// This is necessary because File lock will allow same process to lock file how many times as process wants
25pub(crate) static LOCKED_FILES: Lazy<Mutex<HashSet<String>>> =
26    Lazy::new(|| Mutex::new(HashSet::new()));
27
28/// Stores the chain information and deployment state.
29/// Uses a simple JSON file to store the deployment information locally.
30#[derive(Debug, Clone)]
31pub struct DaemonState {
32    pub json_state: DaemonStateFile,
33    /// Deployment identifier
34    pub deployment_id: String,
35    /// Information about the chain
36    pub chain_data: Arc<ChainInfoOwned>,
37    /// Whether to write on every change of the state
38    pub write_on_change: bool,
39}
40
41impl Drop for DaemonState {
42    fn drop(&mut self) {
43        if let DaemonStateFile::FullAccess { json_file_state } = &self.json_state {
44            let json_lock = json_file_state.lock().unwrap();
45            let mut locked_files = LOCKED_FILES.lock().unwrap();
46            locked_files.remove(json_lock.path());
47        }
48    }
49}
50
51#[derive(Debug, Clone)]
52pub enum DaemonStateFile {
53    ReadOnly {
54        path: String,
55    },
56    FullAccess {
57        json_file_state: Arc<Mutex<JsonLockedState>>,
58    },
59}
60
61impl DaemonState {
62    /// Creates a new state from the given chain data and deployment id.
63    pub fn new(
64        mut json_file_path: String,
65        chain_data: &Arc<ChainInfoOwned>,
66        deployment_id: String,
67        read_only: bool,
68        write_on_change: bool,
69    ) -> Result<DaemonState, DaemonError> {
70        let chain_id = &chain_data.chain_id;
71
72        log::debug!(target: &local_target(), "Using state file : {}", json_file_path);
73
74        // if the network we are connecting is a local kind, add it to the fn
75        if chain_data.kind == ChainKind::Local {
76            let name = Path::new(&json_file_path)
77                .file_stem()
78                .unwrap()
79                .to_str()
80                .unwrap();
81            let folder = Path::new(&json_file_path)
82                .parent()
83                .unwrap()
84                .to_str()
85                .unwrap();
86
87            json_file_path = format!("{folder}/{name}_local.json");
88        }
89
90        let json_state = if read_only {
91            DaemonStateFile::ReadOnly {
92                path: json_file_path,
93            }
94        } else {
95            log::info!(
96                target: &local_target(),
97                "Writing daemon state JSON file: {json_file_path:#?}",
98            );
99
100            let mut lock = LOCKED_FILES.lock().unwrap();
101            if lock.contains(&json_file_path) {
102                return Err(DaemonError::StateAlreadyLocked(json_file_path));
103            }
104            let mut json_file_state = JsonLockedState::new(&json_file_path);
105            // Insert file to a locked files list and drop global mutex lock asap
106            lock.insert(json_file_path);
107            drop(lock);
108
109            json_file_state.prepare(chain_id, &deployment_id);
110            if write_on_change {
111                json_file_state.force_write();
112            }
113            DaemonStateFile::FullAccess {
114                json_file_state: Arc::new(Mutex::new(json_file_state)),
115            }
116        };
117
118        Ok(DaemonState {
119            json_state,
120            deployment_id,
121            chain_data: chain_data.clone(),
122            write_on_change,
123        })
124    }
125
126    /// Returns the path of the file where the state of `cw-orchestrator` is stored.
127    pub fn state_file_path() -> Result<String, DaemonError> {
128        // check if STATE_FILE en var is configured, default to state.json
129        let env_file_path = DaemonEnvVars::state_file();
130        let state_file_path = if env_file_path.is_relative() {
131            // If it's relative, we check if it start with "."
132            let first_path_component = env_file_path
133                .components()
134                .map(|comp| comp.as_os_str().to_owned().into_string().unwrap())
135                .next();
136            if first_path_component == Some(".".to_string()) {
137                let current_dir = std::env::current_dir()?;
138                let actual_relative_path = env_file_path.strip_prefix("./")?;
139                current_dir.join(actual_relative_path)
140            } else if first_path_component == Some("..".to_string()) {
141                let current_dir = std::env::current_dir()?;
142                current_dir.join(env_file_path)
143            } else {
144                let state_folder = default_state_folder()?;
145
146                // We need to create the default state folder if it doesn't exist
147                std::fs::create_dir_all(state_folder.clone())?;
148
149                state_folder.join(env_file_path)
150            }
151        } else {
152            env_file_path
153        }
154        .into_os_string()
155        .into_string()
156        .unwrap();
157
158        Ok(state_file_path)
159    }
160
161    /// Retrieve a stateful value using the chainId and networkId
162    pub fn get(&self, key: &str) -> Result<Value, DaemonError> {
163        let json = match &self.json_state {
164            DaemonStateFile::ReadOnly { path } => {
165                let j = crate::json_lock::read(path)?;
166                let j = patch_state_if_old(j);
167
168                j[&self.chain_data.chain_id].clone()
169            }
170            DaemonStateFile::FullAccess { json_file_state } => json_file_state
171                .lock()
172                .unwrap()
173                .get(&self.chain_data.chain_id)
174                .clone(),
175        };
176        Ok(json[key].clone())
177    }
178
179    /// Set a stateful value using the chainId and networkId
180    pub fn set<T: Serialize>(
181        &mut self,
182        key: &str,
183        contract_id: &str,
184        value: T,
185    ) -> Result<(), DaemonError> {
186        let json_file_state = match &mut self.json_state {
187            DaemonStateFile::ReadOnly { path } => {
188                return Err(DaemonError::StateReadOnly(path.clone()))
189            }
190            DaemonStateFile::FullAccess { json_file_state } => json_file_state,
191        };
192
193        let mut json_file_lock = json_file_state.lock().unwrap();
194        let val = json_file_lock.get_mut(&self.chain_data.chain_id);
195        val[key][contract_id] = json!(value);
196
197        if self.write_on_change {
198            json_file_lock.force_write();
199        }
200
201        Ok(())
202    }
203
204    /// Remove a stateful value using the chainId and networkId
205    pub fn remove(&mut self, key: &str, contract_id: &str) -> Result<(), DaemonError> {
206        let json_file_state = match &mut self.json_state {
207            DaemonStateFile::ReadOnly { path } => {
208                return Err(DaemonError::StateReadOnly(path.clone()))
209            }
210            DaemonStateFile::FullAccess { json_file_state } => json_file_state,
211        };
212
213        let mut json_file_lock = json_file_state.lock().unwrap();
214        let val = json_file_lock.get_mut(&self.chain_data.chain_id);
215        val[key][contract_id] = Value::Null;
216
217        if self.write_on_change {
218            json_file_lock.force_write();
219        }
220
221        Ok(())
222    }
223
224    /// Forcefully write current json to a file
225    pub fn force_write(&mut self) -> Result<(), DaemonError> {
226        let json_file_state = match &mut self.json_state {
227            DaemonStateFile::ReadOnly { path } => {
228                return Err(DaemonError::StateReadOnly(path.clone()))
229            }
230            DaemonStateFile::FullAccess { json_file_state } => json_file_state,
231        };
232        json_file_state.lock().unwrap().force_write();
233        Ok(())
234    }
235
236    /// Flushes all the state related to the current chain
237    /// Only works on Local networks
238    pub fn flush(&mut self) -> Result<(), DaemonError> {
239        if self.chain_data.kind != ChainKind::Local {
240            panic!("Can only flush local chain state");
241        }
242        let json_file_state = match &mut self.json_state {
243            DaemonStateFile::ReadOnly { path } => {
244                return Err(DaemonError::StateReadOnly(path.clone()))
245            }
246            DaemonStateFile::FullAccess { json_file_state } => json_file_state,
247        };
248
249        let mut json_file_lock = json_file_state.lock().unwrap();
250        let json = json_file_lock.get_mut(&self.chain_data.chain_id);
251
252        *json = json!({});
253
254        if self.write_on_change {
255            json_file_lock.force_write();
256        }
257        Ok(())
258    }
259}
260
261impl StateInterface for DaemonState {
262    /// Read address for contract in deployment id from state file
263    fn get_address(&self, contract_id: &str) -> Result<Addr, CwEnvError> {
264        let value = self
265            .get(&self.deployment_id)
266            .ok()
267            .and_then(|v| v.get(contract_id).cloned())
268            .ok_or_else(|| CwEnvError::AddrNotInStore(contract_id.to_owned()))?
269            .clone();
270        Ok(Addr::unchecked(value.as_str().unwrap()))
271    }
272
273    /// Set address for contract in deployment id in state file
274    fn set_address(&mut self, contract_id: &str, address: &Addr) {
275        let deployment_id = self.deployment_id.clone();
276        self.set(&deployment_id, contract_id, address.as_str())
277            .unwrap();
278    }
279
280    fn remove_address(&mut self, contract_id: &str) {
281        let deployment_id = self.deployment_id.clone();
282        self.remove(&deployment_id, contract_id).unwrap();
283    }
284
285    /// Get the locally-saved version of the contract's version on this network
286    fn get_code_id(&self, contract_id: &str) -> Result<u64, CwEnvError> {
287        let value = self
288            .get("code_ids")
289            .ok()
290            .and_then(|v| v.get(contract_id).cloned())
291            .ok_or_else(|| CwEnvError::CodeIdNotInStore(contract_id.to_owned()))?
292            .clone();
293        Ok(value.as_u64().unwrap())
294    }
295
296    /// Set the locally-saved version of the contract's latest version on this network
297    fn set_code_id(&mut self, contract_id: &str, code_id: u64) {
298        self.set("code_ids", contract_id, code_id).unwrap();
299    }
300    fn remove_code_id(&mut self, contract_id: &str) {
301        self.remove("code_ids", contract_id).unwrap();
302    }
303
304    /// Get all addresses for deployment id from state file
305    fn get_all_addresses(&self) -> Result<HashMap<String, Addr>, CwEnvError> {
306        let mut store = HashMap::new();
307        let addresses = self.get(&self.deployment_id)?;
308        let value = addresses.as_object().cloned().unwrap_or_default();
309        for (id, addr) in value {
310            store.insert(id, Addr::unchecked(addr.as_str().unwrap()));
311        }
312        Ok(store)
313    }
314
315    fn get_all_code_ids(&self) -> Result<HashMap<String, u64>, CwEnvError> {
316        let mut store = HashMap::new();
317        let code_ids = self.get("code_ids")?;
318        let value = code_ids.as_object().cloned().unwrap_or_default();
319        for (id, code_id) in value {
320            store.insert(id, code_id.as_u64().unwrap());
321        }
322        Ok(store)
323    }
324}
325
326pub trait DeployedChains<Chain: CwEnv>: cw_orch_core::contract::Deploy<Chain> {
327    /// Gets all the chain ids on which the library is deployed on
328    /// This loads all chains that are registered in the crate-local daemon_state file
329    /// The state file should have the following format :
330    /// {
331    ///     "juno":{
332    ///         "juno-1":{
333    ///             ...
334    ///         },
335    ///         "uni-6": {
336    ///         }
337    ///     }
338    ///     ...
339    /// }
340    /// So this function actually looks for the second level of indices in the deployed_state_file
341    fn get_all_deployed_chains() -> Vec<String> {
342        let deployed_state_file = Self::deployed_state_file_path();
343        if let Some(state_file) = deployed_state_file {
344            if let Ok(module_state_json) = crate::json_lock::read(&state_file) {
345                let module_state_json = patch_state_if_old(module_state_json);
346                return module_state_json
347                    .as_object()
348                    .unwrap()
349                    .keys()
350                    .cloned()
351                    .collect();
352            }
353        }
354        vec![]
355    }
356    /// Set the default contract state for a contract, so that users can retrieve it in their application when importing the library
357    /// If a state is provided, it is used for all contracts, otherwise, the state is loaded from the crate's state file.
358    fn set_contracts_state(&mut self, custom_state: Option<Value>) {
359        let mut is_loading_from_file = false;
360        let Some(maybe_old_state) = custom_state.or_else(|| {
361            is_loading_from_file = true;
362            Self::deployed_state_file_path()
363                .and_then(|state_file| crate::json_lock::read(&state_file).ok())
364        }) else {
365            return;
366        };
367        let state = patch_state_if_old(maybe_old_state);
368
369        let all_contracts = self.get_contracts_mut();
370
371        for contract in all_contracts {
372            // If we're loading the state from file and the environment doesn't allow that, we don't load addresses and code-ids
373            if is_loading_from_file && !contract.environment().can_load_state_from_state_file() {
374                return;
375            }
376
377            // We set the code_id and/or address of the contract in question if they are not present already
378            let env_info = contract.environment().env_info();
379            // We load the file
380            // We try to get the code_id for the contract
381            if contract.code_id().is_err() {
382                let code_id = state
383                    .get(env_info.chain_id.to_string())
384                    .unwrap_or(&Value::Null)
385                    .get("code_ids")
386                    .unwrap_or(&Value::Null)
387                    .get(contract.id());
388
389                if let Some(code_id) = code_id {
390                    if code_id.is_u64() {
391                        contract.set_default_code_id(code_id.as_u64().unwrap())
392                    }
393                }
394            }
395            // We try to get the address for the contract
396            if contract.address().is_err() {
397                // Try and get the code id from file
398                let address = state
399                    .get(env_info.chain_id.to_string())
400                    .unwrap_or(&Value::Null)
401                    .get(env_info.deployment_id)
402                    .unwrap_or(&Value::Null)
403                    .get(contract.id());
404
405                if let Some(address) = address {
406                    if address.is_string() {
407                        contract.set_default_address(&Addr::unchecked(address.as_str().unwrap()))
408                    }
409                }
410            }
411        }
412    }
413
414    /// Sets the custom state file path for exporting the state with the package.
415    /// This function needs to be defined by projects. If the project doesn't want to give deployment state with their crate, they can return None here.
416    fn deployed_state_file_path() -> Option<String>;
417}
418
419pub(crate) use tempstate::gen_temp_file_path;
420
421mod tempstate {
422
423    use uid::IdU16 as IdT;
424
425    #[derive(Copy, Clone)]
426    struct T(());
427
428    type Id = IdT<T>;
429
430    pub fn gen_temp_file_path() -> std::path::PathBuf {
431        let id = Id::new();
432        let id = id.get();
433        let env_dir = std::env::temp_dir();
434        env_dir.join(format!("tempstate_{id}"))
435    }
436}
437
438#[cfg(test)]
439pub mod test {
440    use std::env;
441
442    use crate::{env::STATE_FILE_ENV_NAME, DaemonState};
443
444    #[test]
445    #[serial_test::serial]
446    fn test_env_variable_state_path() -> anyhow::Result<()> {
447        let absolute_path = "/usr/var/file.json";
448        let relative_path = "folder/file.json";
449        let dotted_relative_path = format!("./{}", relative_path);
450        let parent_and_relative_path = format!("../{}", relative_path);
451
452        std::env::set_var(STATE_FILE_ENV_NAME, absolute_path);
453        let absolute_state_path = DaemonState::state_file_path()?;
454        assert_eq!(absolute_path.to_string(), absolute_state_path);
455
456        std::env::set_var(STATE_FILE_ENV_NAME, dotted_relative_path);
457        let relative_state_path = DaemonState::state_file_path()?;
458        assert_eq!(
459            env::current_dir()?
460                .join(relative_path)
461                .into_os_string()
462                .into_string()
463                .unwrap(),
464            relative_state_path
465        );
466
467        std::env::set_var(STATE_FILE_ENV_NAME, relative_path);
468        let relative_state_path = DaemonState::state_file_path()?;
469        assert_eq!(
470            dirs::home_dir()
471                .unwrap()
472                .join(".cw-orchestrator")
473                .join(relative_path)
474                .into_os_string()
475                .into_string()
476                .unwrap(),
477            relative_state_path
478        );
479
480        std::env::set_var(STATE_FILE_ENV_NAME, parent_and_relative_path);
481        let parent_and_relative_state_path = DaemonState::state_file_path()?;
482        assert_eq!(
483            env::current_dir()?
484                .join("../")
485                .join(relative_path)
486                .into_os_string()
487                .into_string()
488                .unwrap(),
489            parent_and_relative_state_path
490        );
491
492        std::env::remove_var(STATE_FILE_ENV_NAME);
493        Ok(())
494    }
495}