use crate::config::turnkey::{Config, StoredQosOperatorKey};
use crate::pair::{HexSeed, LocalPair};
use anyhow::{Context, anyhow, bail};
use std::path::PathBuf;
#[derive(Debug)]
pub enum LocalOperatorSeedSource {
Value(HexSeed),
Path(PathBuf),
}
impl LocalOperatorSeedSource {
pub fn from_args(seed: Option<HexSeed>, path: Option<PathBuf>) -> anyhow::Result<Option<Self>> {
match (seed, path) {
(Some(_), Some(_)) => {
bail!(
"--operator-seed and --operator-seed-path are mutually exclusive, \
please provide only one"
)
}
(Some(seed), None) => Ok(Some(Self::Value(seed))),
(None, Some(path)) => Ok(Some(Self::Path(path))),
(None, None) => Ok(None),
}
}
}
#[derive(Debug)]
pub enum LocalCredentialSource {
RegisteredKeyFile(PathBuf),
Explicit(LocalOperatorSeedSource),
}
pub async fn resolve_local_operator(
explicit: Option<LocalOperatorSeedSource>,
) -> anyhow::Result<LocalPair> {
let source = match explicit {
Some(source) => LocalCredentialSource::Explicit(source),
None => {
let config = Config::load().await?;
let (alias, org_config) = config.active_org_config().ok_or_else(|| {
anyhow!(
"No active organization. Run `tvc login` first or provide \
--operator-seed or --operator-seed-path."
)
})?;
let local = org_config.select_local_record(alias)?;
return resolve_registered_local_operator(local.key_path.clone()).await;
}
};
resolve_local_credential(source).await
}
pub(crate) async fn resolve_registered_local_operator(
key_path: PathBuf,
) -> anyhow::Result<LocalPair> {
resolve_local_credential(LocalCredentialSource::RegisteredKeyFile(key_path)).await
}
async fn resolve_local_credential(source: LocalCredentialSource) -> anyhow::Result<LocalPair> {
match source {
LocalCredentialSource::Explicit(LocalOperatorSeedSource::Value(seed)) => {
LocalPair::from_seed(&seed)
}
LocalCredentialSource::Explicit(LocalOperatorSeedSource::Path(path)) => {
LocalPair::from_master_seed(&path).await
}
LocalCredentialSource::RegisteredKeyFile(path) => {
let key = StoredQosOperatorKey::load(&path).await?.ok_or_else(|| {
anyhow!(
"No operator key found at '{}'. Run `tvc login` first or provide \
--operator-seed or --operator-seed-path.",
path.display()
)
})?;
LocalPair::from_hex_seed(&key.private_key)
.with_context(|| format!("failed to load operator key: {}", path.display()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::turnkey::StoredQosOperatorKey;
use crate::pair::Pair;
use std::fs;
use tempfile::TempDir;
fn seed() -> HexSeed {
"ab".repeat(32).parse().unwrap()
}
#[test]
fn from_args_with_neither_is_none() {
assert!(
LocalOperatorSeedSource::from_args(None, None)
.unwrap()
.is_none()
);
}
#[test]
fn from_args_with_seed_is_value() {
let source = LocalOperatorSeedSource::from_args(Some(seed()), None).unwrap();
assert!(matches!(source, Some(LocalOperatorSeedSource::Value(_))));
}
#[test]
fn from_args_with_path_is_path() {
let source =
LocalOperatorSeedSource::from_args(None, Some(PathBuf::from("seed.hex"))).unwrap();
assert!(matches!(source, Some(LocalOperatorSeedSource::Path(_))));
}
#[test]
fn from_args_with_both_is_an_error() {
let err = LocalOperatorSeedSource::from_args(Some(seed()), Some(PathBuf::from("seed.hex")))
.unwrap_err();
assert!(err.to_string().contains("mutually exclusive"));
}
#[tokio::test]
async fn registered_source_parses_stored_key_json() {
let temp = TempDir::new().unwrap();
let path = temp.path().join("operator.json");
let private_key = "ab".repeat(32);
fs::write(
&path,
serde_json::to_string(&StoredQosOperatorKey {
public_key: "unused".to_string(),
private_key: private_key.clone(),
})
.unwrap(),
)
.unwrap();
let pair = resolve_local_credential(LocalCredentialSource::RegisteredKeyFile(path))
.await
.unwrap();
let expected = LocalPair::from_hex_seed(&private_key).unwrap();
assert_eq!(pair.public_key(), expected.public_key());
}
#[tokio::test]
async fn explicit_raw_hex_path_uses_raw_seed_parser() {
let temp = TempDir::new().unwrap();
let path = temp.path().join("seed.hex");
let private_key = "cd".repeat(32);
fs::write(&path, format!(" 0x{private_key}\n")).unwrap();
let pair = resolve_local_credential(LocalCredentialSource::Explicit(
LocalOperatorSeedSource::Path(path),
))
.await
.unwrap();
assert_eq!(
pair.public_key(),
LocalPair::from_hex_seed(&private_key).unwrap().public_key()
);
}
#[tokio::test]
async fn explicit_value_resolves_without_config() {
resolve_local_operator(Some(LocalOperatorSeedSource::Value(seed())))
.await
.unwrap();
}
}