use crate::client::build_client;
use crate::config::app::{AppConfig, OperatorSetParams};
use crate::config::turnkey;
use anyhow::{Context, Result};
use clap::Args as ClapArgs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use turnkey_client::generated::{CreateTvcAppIntent, TvcOperatorParams, TvcOperatorSetParams};
#[derive(Debug, ClapArgs)]
#[command(about, long_about = None)]
pub struct Args {
#[arg(short = 'c', long, value_name = "PATH", env = "TVC_APP_CONFIG")]
pub config_file: PathBuf,
}
pub async fn run(args: Args) -> Result<()> {
let config_content = std::fs::read_to_string(&args.config_file)
.with_context(|| format!("failed to read config file: {}", args.config_file.display()))?;
let app_config: AppConfig = serde_json::from_str(&config_content).with_context(|| {
format!(
"failed to parse config file: {}",
args.config_file.display()
)
})?;
app_config
.validate()
.with_context(|| format!("invalid config file: {}", args.config_file.display()))?;
println!("Creating app '{}'...", app_config.name);
let auth = build_client().await?;
let intent = build_create_tvc_app_intent(&app_config);
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system time before unix epoch")?
.as_millis();
let result = auth
.client
.create_tvc_app(auth.org_id, timestamp_ms, intent)
.await
.context("failed to create TVC app")?;
let app_id = result.result.app_id;
let operator_ids = result.result.manifest_set_operator_ids;
let mut config = turnkey::Config::load().await?;
config.set_last_app_id(&app_id)?;
config.set_last_operator_ids(&operator_ids)?;
config.save().await?;
println!();
println!("App created successfully!");
println!();
println!("App ID: {app_id}");
println!("Name: {}", app_config.name);
println!("Manifest Set ID: {}", result.result.manifest_set_id);
if !operator_ids.is_empty() {
println!("Manifest Set Operator IDs: {}", operator_ids.join(", "));
}
println!("Config: {}", args.config_file.display());
println!();
println!(
"Use one of the Manifest Set Operator IDs above with `tvc deploy approve --operator-id`"
);
Ok(())
}
fn build_create_tvc_app_intent(app_config: &AppConfig) -> CreateTvcAppIntent {
let share_set_params = app_config.effective_share_set_params();
CreateTvcAppIntent {
name: app_config.name.clone(),
quorum_public_key: app_config.quorum_public_key.clone(),
manifest_set_id: app_config.manifest_set_id.clone(),
manifest_set_params: app_config
.manifest_set_params
.as_ref()
.map(to_tvc_operator_set_params),
share_set_id: app_config.share_set_id.clone(),
share_set_params: share_set_params.as_ref().map(to_tvc_operator_set_params),
enable_egress: app_config.external_connectivity,
}
}
fn to_tvc_operator_set_params(params: &OperatorSetParams) -> TvcOperatorSetParams {
TvcOperatorSetParams {
name: params.name.clone(),
threshold: params.threshold,
new_operators: params
.new_operators
.iter()
.map(|o| TvcOperatorParams {
name: o.name.clone(),
public_key: o.public_key.clone(),
})
.collect(),
existing_operator_ids: params.existing_operator_ids.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::app::{OperatorParams, KNOWN_QUORUM_KEY};
fn valid_config() -> AppConfig {
AppConfig {
name: "test-app".to_string(),
quorum_public_key: KNOWN_QUORUM_KEY.to_string(),
external_connectivity: Some(false),
manifest_set_id: None,
manifest_set_params: Some(OperatorSetParams {
name: "manifest-set".to_string(),
threshold: 1,
new_operators: vec![OperatorParams {
name: "manifest-operator".to_string(),
public_key: "manifest-public-key".to_string(),
}],
existing_operator_ids: vec![],
}),
share_set_id: None,
share_set_params: None,
}
}
#[test]
fn build_intent_uses_default_share_set_params_when_omitted() {
let intent = build_create_tvc_app_intent(&valid_config());
let share_set_params = intent.share_set_params.unwrap();
assert_eq!(share_set_params.name, "dev-known-share-set");
assert_eq!(share_set_params.threshold, 2);
assert_eq!(share_set_params.new_operators.len(), 2);
assert!(share_set_params.existing_operator_ids.is_empty());
}
#[test]
fn build_intent_uses_custom_share_set_params_when_configured() {
let mut config = valid_config();
config.share_set_params = Some(OperatorSetParams {
name: "custom-share-set".to_string(),
threshold: 2,
new_operators: vec![OperatorParams {
name: "share-operator".to_string(),
public_key: "share-public-key".to_string(),
}],
existing_operator_ids: vec!["existing-operator-id".to_string()],
});
let intent = build_create_tvc_app_intent(&config);
let share_set_params = intent.share_set_params.unwrap();
assert_eq!(share_set_params.name, "custom-share-set");
assert_eq!(share_set_params.threshold, 2);
assert_eq!(share_set_params.new_operators[0].name, "share-operator");
assert_eq!(
share_set_params.existing_operator_ids,
vec!["existing-operator-id".to_string()]
);
}
#[test]
fn build_intent_uses_share_set_id_when_configured() {
let mut config = valid_config();
config.share_set_id = Some("share-set-id".to_string());
let intent = build_create_tvc_app_intent(&config);
assert_eq!(intent.share_set_id.as_deref(), Some("share-set-id"));
assert!(intent.share_set_params.is_none());
}
}