relay-cli 0.1.2

CLI interface for Relay Agents.
use anyhow::Result;
use serde::Serialize;

use crate::{
    args::Args,
    parsers::parse_address,
    progress::Progress,
    storage::{Identity, Storage},
    transport::agent_base_url,
};
use relay_lib::{
    core::chrono::{DateTime, Utc},
    crypto::{PublicKey, SigningKey, StaticSecret, hex},
    prelude::{Address, KeyRecord},
};

#[derive(Serialize)]
struct ClaimUserReq {
    pub address: String,
    #[serde(with = "hex::serde")]
    pub ed25519: [u8; 32],
    #[serde(with = "hex::serde")]
    pub x25519: [u8; 32],
    pub expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, clap::Parser)]
#[clap(about = "Claim a new identity on the Agent")]
pub struct ClaimCmd {
    #[arg(value_parser = parse_address)]
    pub address: Address,

    #[clap(short, long, default_value_t = false)]
    pub replace: bool,
}

impl ClaimCmd {
    pub fn execute(&self, _: Args, storage: &mut Storage) -> Result<()> {
        let mut progress = Progress::new(false);

        if self.address.inbox().is_some() {
            progress.commit();
            progress.error("Cannot claim identity with an inbox");
            progress.warn("Please provide an address without an inbox to claim an identity, or use `claim-inbox` to claim an inbox.");
            progress.abort("Claim command only works for addresses without inboxes");
        }
        let mut address = self.address.clone();
        address.inbox = None;

        progress.step(
            &format!("Checking existing identity for `{}`", address),
            "Checked existing identity",
        );
        if storage.root.get_identity(&self.address).is_some() {
            if !self.replace {
                progress.error(&format!(
                    "Identity for address `{}` already exists locally. Use --replace to overwrite.",
                    address,
                ));
                progress.warn("THIS WILL DELETE THE EXISTING IDENTITY AND ITS PRIVATE KEYS!");
                progress.warn("THIS CAN NOT BE UNDONE!");
                return Ok(());
            } else {
                progress.warn(&format!(
                    "Replacing existing identity for address `{}`",
                    address,
                ));
            }
        }
        progress.step("Generating new identity", "Generated new identity");
        let identity = Self::generate_identity(&address);

        progress.step("Requsting identity claim", "Identity claimed");
        let resp = reqwest::blocking::Client::new()
            .post(agent_base_url(address.agent())?.join("/user/claim/user")?)
            .json(&ClaimUserReq {
                address: address.canonical(),
                ed25519: identity.pub_record.ed25519,
                x25519: identity.pub_record.x25519,
                expires_at: identity.pub_record.expires_at,
            })
            .send()
            .map_err(|_| {
                progress.abort("Failed to send identity claim request");
            })?;

        if resp.status().as_u16() == 409 {
            progress.abort("Identity already claimed on the Agent");
        }

        if !resp.status().is_success() {
            let status = resp.status();
            progress.abort(&format!("Failed to claim identity. Status: {}", status));
        }

        progress.step("Storing identity locally", "Identity stored locally");
        storage.root.identities.insert(address.clone(), identity);

        progress.commit();
        progress.arrow(&format!("Successfully claimed identity `{}`", address));
        Ok(())
    }

    fn generate_identity(address: &Address) -> Identity {
        let mut rng = relay_lib::crypto::OsRng;

        let signing_key = SigningKey::generate(&mut rng);
        let static_secret = StaticSecret::random_from_rng(rng);
        let public_key = PublicKey::from(&static_secret);

        let pub_record = KeyRecord {
            id: address.canonical().to_string(),
            version: 1,
            ed25519: signing_key.verifying_key().to_bytes(),
            x25519: public_key.to_bytes(),
            created_at: Utc::now(),
            expires_at: None,
        };

        Identity {
            pub_record,
            inboxes: vec![],
            signing_key: signing_key.to_bytes(),
            static_secret: signing_key.to_bytes(),
        }
    }
}