casper-devnet 0.10.0

Launcher for local Casper Network development networks.
Documentation
use crate::assets;
use anyhow::Result;
use directories::BaseDirs;
use serde::Deserialize;
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::fs as tokio_fs;

pub(crate) const DEFAULT_SEED: &str = "default";

pub(crate) fn shorten_home_path(path: &str) -> String {
    let path = Path::new(path);
    let Some(base_dirs) = BaseDirs::new() else {
        return path.display().to_string();
    };
    let home = base_dirs.home_dir();
    match path.strip_prefix(home) {
        Ok(stripped) => {
            if stripped.as_os_str().is_empty() {
                return "~".to_string();
            }
            let mut shorthand = PathBuf::from("~");
            shorthand.push(stripped);
            shorthand.display().to_string()
        }
        Err(_) => path.display().to_string(),
    }
}

pub(crate) async fn is_dir(path: &Path) -> bool {
    tokio_fs::metadata(path)
        .await
        .map(|meta| meta.is_dir())
        .unwrap_or(false)
}

pub(crate) async fn is_file(path: &Path) -> bool {
    tokio_fs::metadata(path)
        .await
        .map(|meta| meta.is_file())
        .unwrap_or(false)
}

pub(crate) async fn is_control_socket(path: &Path) -> bool {
    tokio_fs::symlink_metadata(path)
        .await
        .map(|metadata| metadata.file_type().is_socket())
        .unwrap_or(false)
}

pub(crate) async fn read_current_era_from_status(node_id: u32) -> Result<Option<u64>> {
    let client = reqwest::Client::builder()
        .no_proxy()
        .timeout(Duration::from_secs(2))
        .build()?;
    let url = format!("{}/status", assets::rest_endpoint(node_id));
    let response = client.get(&url).send().await?;
    if response.status() != reqwest::StatusCode::OK {
        return Ok(None);
    }
    let status = response.json::<NodeStatus>().await?;
    Ok(extract_era_id(status.last_added_block_info.as_ref()))
}

pub(crate) fn extract_era_id(value: Option<&serde_json::Value>) -> Option<u64> {
    let value = value?;
    if let Some(era_id) = value.get("era_id").and_then(|era_id| era_id.as_u64()) {
        return Some(era_id);
    }
    value
        .get("era_id")
        .and_then(|era_id| era_id.as_str())
        .and_then(|era_id| era_id.parse::<u64>().ok())
}

#[derive(Deserialize)]
#[allow(dead_code)]
#[serde(rename_all = "snake_case")]
struct NodeStatus {
    last_added_block_info: Option<serde_json::Value>,
}

pub(crate) fn is_ready_reactor_state(state: &str) -> bool {
    state == "Validate"
}

#[cfg(test)]
mod tests {
    use super::{extract_era_id, shorten_home_path};
    use directories::BaseDirs;
    use serde_json::json;

    #[test]
    fn shorten_home_path_replaces_home_prefix() {
        let Some(base_dirs) = BaseDirs::new() else {
            return;
        };
        let home = base_dirs.home_dir();
        let shortened = shorten_home_path(&home.to_string_lossy());
        assert_eq!(shortened, "~");

        let nested = home.join("devnet/logs/stdout.log");
        let shortened_nested = shorten_home_path(&nested.to_string_lossy());
        assert!(shortened_nested.starts_with("~"));
        assert!(shortened_nested.contains("devnet"));
    }

    #[test]
    fn shorten_home_path_keeps_relative_paths() {
        let input = "relative/path";
        assert_eq!(shorten_home_path(input), input);
    }

    #[test]
    fn extract_era_id_from_status_payload() {
        assert_eq!(extract_era_id(Some(&json!({"era_id": 123}))), Some(123));
        assert_eq!(extract_era_id(Some(&json!({"era_id": "456"}))), Some(456));
        assert_eq!(extract_era_id(Some(&json!({"era_id": "abc"}))), None);
        assert_eq!(extract_era_id(None), None);
    }
}