dee 0.0.18

An cli for drand, with support for timelock encryption.
use anyhow::{anyhow, Result};
use colored::Colorize;
use drand_core::HttpClient;
use log::{log_enabled, Level};
use time::OffsetDateTime;

use crate::{
    config::{self, ConfigChain},
    print::{self, print_with_format},
};

pub fn add(cfg: &mut config::Local, name: String, url: &str) -> Result<String> {
    if cfg.chain(&name).is_some() {
        return Err(anyhow!("remote {name} already exists."));
    }
    let client: HttpClient = url.try_into()?;
    let info = client.chain_info().map_err(|err| {
        anyhow!("failed to retrieve information from remote '{name}'. server response: {err}")
    })?;

    cfg.add_chain(name.clone(), ConfigChain::new(url, info))?;

    Ok(name)
}

pub fn remove(cfg: &mut config::Local, name: String) -> Result<String> {
    if cfg.chain(&name).is_none() {
        return Err(anyhow!("no such remote '{name}'."));
    }
    cfg.remove_chain(name.clone())?;

    Ok(name)
}

pub fn rename(cfg: &mut config::Local, old: String, new: String) -> Result<String> {
    if cfg.chain(&old).is_none() {
        return Err(anyhow!("no such remote '{old}'."));
    }
    if cfg.chain(&new).is_some() {
        return Err(anyhow!("remote {new} already exists."));
    }

    cfg.rename_chain(old.clone(), new.clone())?;

    if let Some(upstream) = cfg.upstream() {
        if upstream == old {
            cfg.set_upstream(&new)?;
        }
    }

    Ok(new)
}

pub fn set_url(cfg: &mut config::Local, name: String, url: &str) -> Result<String> {
    if cfg.chain(&name).is_none() {
        return Err(anyhow!("no such remote '{name}'."));
    }
    cfg.set_url_chain(name.clone(), url)?;

    Ok(name)
}

impl print::Print for ConfigChain {
    fn short(&self) -> Result<String> {
        Ok(hex::encode(self.info().public_key()))
    }

    fn long(&self) -> Result<String> {
        let info = self.info();
        Ok(format!(
            r"{: <10}: {}
{: <10}: {}
{: <10}: {}s
{: <10}: {}
{: <10}: {}
{: <10}: {}
{: <10}: {}
{: <10}: {}",
            "URL".bold(),
            self.url(),
            "Public Key".bold(),
            hex::encode(info.public_key()),
            "Period".bold(),
            info.period(),
            "Genesis".bold(),
            OffsetDateTime::from_unix_timestamp(info.genesis_time() as i64).unwrap(),
            "Chain Hash".bold(),
            hex::encode(info.hash()),
            "Group Hash".bold(),
            hex::encode(info.group_hash()),
            "Scheme ID".bold(),
            info.scheme_id(),
            "Beacon ID".bold(),
            info.metadata().beacon_id()
        ))
    }

    fn json(&self) -> Result<String> {
        serde_json::to_string(&self.info()).map_err(|e| anyhow!(e))
    }
}

pub fn show(cfg: &config::Local, format: print::Format, name: String) -> Result<String> {
    let chain = match cfg.chain(&name) {
        Some(chain) => chain,
        None => return Err(anyhow!("no such remote '{name}'.")),
    };

    print_with_format(chain, format)
}

pub fn list(cfg: &config::Local) -> Result<String> {
    let chains: Vec<String> = cfg.chains().keys().cloned().collect();
    if chains.is_empty() {
        Ok("".into())
    } else {
        let output: Vec<String> = chains
            .iter()
            .map(|key| (key.to_owned(), cfg.chain(key.as_str()).unwrap()))
            .map(|(name, chain)| {
                if log_enabled!(Level::Warn) {
                    format!("{name: <20}\t{url}", url = chain.url())
                } else {
                    name
                }
            })
            .collect();
        Ok(output.join("\n"))
    }
}