use std::fmt::Display;
use crate::prompts;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const MIN_SHARE_SET_THRESHOLD: u32 = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppConfig {
pub name: String,
pub quorum_public_key: String,
#[serde(default)]
pub enable_egress: bool,
#[serde(default)]
pub manifest_set_id: Option<String>,
#[serde(default)]
pub manifest_set_params: Option<OperatorSetParams>,
#[serde(default)]
pub share_set_id: Option<String>,
#[serde(default)]
pub share_set_params: Option<OperatorSetParams>,
#[serde(default)]
pub dangerous_enable_debug_mode_deployments: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperatorSetParams {
pub name: String,
pub threshold: u32,
#[serde(default)]
pub new_operators: Vec<OperatorParams>,
#[serde(default)]
pub existing_operator_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperatorParams {
pub name: String,
pub public_key: String,
}
pub const KNOWN_SHARE_SET_KEYS: [(&str, &str); 2] = [
(
"1",
"044af8b082b9ef41a238037811a188309d8c8b00b6d49c0574538d7746d7383739e67e1107f134bc102a48301b07e7c53280decbe9c16c9fc1f19b9832018e1485048139aa5de49d9505465bcf1a879954c51ba7b258b669f4e42697088cbbca54aeb888d61e65b2602ce92ae945a0160533acc94942511f8e5b1940ed89cc8f141f",
),
(
"2",
"04c1c4b4eb784505f167affae00e18b1521e7a0bfa3be46e6a6b43ba1f386afce48d964c885480cb197e3538fd30ebe38a07f76b6a286b37ba6d2abddbbd6c9c8304e492ca7bce95912a7b2565c8553e38cf3a4b1f858171900ed81888282db13d41e214dd6def2de2aacb1fcf92e3ae5a83e1b0ffa660fc59b9dd10e277cfd128dc",
),
];
pub const KNOWN_QUORUM_KEY: &str = "04451028fc9d42cef6d8f2a3ebe17d65783c470dbc6f04663d500c12009930cf9b209e733f6ac6103cc28f07ecde2dbb55095738b828d6b7a55caf4ddf9d67f2ae047827dcd2325b8d58694c2ea14e8f1e1f8a36c84438d291ff9b1b067debdb3e2ba3822984cde8bed4de2c237bd323526da4961d368bcc63cbd2d37d00e936683e";
impl AppConfig {
pub fn template(operator_public_key: Option<&str>) -> Self {
Self {
name: "<FILL_IN_APP_NAME>".to_string(),
quorum_public_key: KNOWN_QUORUM_KEY.to_string(),
enable_egress: false,
manifest_set_id: None,
manifest_set_params: Some(OperatorSetParams {
name: "<FILL_IN_MANIFEST_SET_NAME>".to_string(),
threshold: 1,
new_operators: vec![OperatorParams {
name: "operator-1".to_string(),
public_key: operator_public_key
.unwrap_or("<FILL_IN_OPERATOR_PUBLIC_KEY>")
.to_string(),
}],
existing_operator_ids: vec![],
}),
share_set_id: None,
share_set_params: None,
dangerous_enable_debug_mode_deployments: false,
}
}
pub fn share_set_params() -> OperatorSetParams {
OperatorSetParams {
name: "dev-known-share-set".to_string(),
threshold: 2,
new_operators: KNOWN_SHARE_SET_KEYS
.iter()
.map(|(name, key)| OperatorParams {
name: name.to_string(),
public_key: key.to_string(),
})
.collect(),
existing_operator_ids: vec![],
}
}
pub fn fill_interactively(&mut self, saved_operator_public_key: Option<&str>) -> Result<()> {
if self.name.starts_with("<FILL_IN") {
self.name = prompts::required_text("App name", None)?;
}
if let Some(set_params) = self.manifest_set_params.as_mut() {
if set_params.name.starts_with("<FILL_IN") {
set_params.name = prompts::required_text("Manifest set name", None)?;
}
for op in set_params.new_operators.iter_mut() {
if op.public_key.starts_with("<FILL_IN") {
let prompt = format!("Operator '{}' public key", op.name);
op.public_key = prompts::required_text(&prompt, saved_operator_public_key)?;
}
}
}
if let Some(set_params) = self.share_set_params.as_mut() {
if set_params.name.starts_with("<FILL_IN") {
set_params.name = prompts::required_text("Share set name", None)?;
}
for op in set_params.new_operators.iter_mut() {
if op.public_key.starts_with("<FILL_IN") {
let prompt = format!("Share set operator '{}' public key", op.name);
op.public_key = prompts::required_text(&prompt, None)?;
}
}
}
Ok(())
}
pub fn has_placeholders(&self) -> bool {
self.name.starts_with("<FILL_IN")
|| self.manifest_set_params.as_ref().is_some_and(|p| {
p.name.starts_with("<FILL_IN")
|| p.new_operators
.iter()
.any(|o| o.public_key.starts_with("<FILL_IN"))
})
|| self.share_set_params.as_ref().is_some_and(|p| {
p.name.starts_with("<FILL_IN")
|| p.new_operators
.iter()
.any(|o| o.public_key.starts_with("<FILL_IN"))
})
}
pub fn validate(&self) -> Result<(), AppConfigValidationErrors> {
let mut errors = Vec::new();
if self.name.starts_with("<FILL_IN") {
errors.push(AppConfigValidationError::Placeholder {
field: "name",
placeholder: self.name.clone(),
});
}
if let Some(params) = &self.manifest_set_params {
collect_operator_set_placeholder_errors("manifestSetParams", params, &mut errors);
}
if let Some(params) = &self.share_set_params {
collect_operator_set_placeholder_errors("shareSetParams", params, &mut errors);
}
if self.manifest_set_id.is_some() && self.manifest_set_params.is_some() {
errors.push(AppConfigValidationError::ConflictingFields {
first: "manifestSetId",
second: "manifestSetParams",
});
}
if self.manifest_set_id.is_none() && self.manifest_set_params.is_none() {
errors.push(AppConfigValidationError::MissingOneOf {
first: "manifestSetId",
second: "manifestSetParams",
});
}
if self.share_set_id.is_some() && self.share_set_params.is_some() {
errors.push(AppConfigValidationError::ConflictingFields {
first: "shareSetId",
second: "shareSetParams",
});
}
if let Some(params) = &self.share_set_params
&& params.threshold < MIN_SHARE_SET_THRESHOLD
{
errors.push(AppConfigValidationError::ThresholdTooLow {
field: "shareSetParams.threshold",
minimum: MIN_SHARE_SET_THRESHOLD,
actual: params.threshold,
});
}
AppConfigValidationErrors::ok_or_errors(errors)
}
pub fn effective_share_set_params(&self) -> Option<OperatorSetParams> {
if self.share_set_id.is_some() {
None
} else {
Some(
self.share_set_params
.clone()
.unwrap_or_else(Self::share_set_params),
)
}
}
}
fn collect_operator_set_placeholder_errors(
prefix: &'static str,
params: &OperatorSetParams,
errors: &mut Vec<AppConfigValidationError>,
) {
if params.name.starts_with("<FILL_IN") {
errors.push(AppConfigValidationError::Placeholder {
field: if prefix == "manifestSetParams" {
"manifestSetParams.name"
} else {
"shareSetParams.name"
},
placeholder: params.name.clone(),
});
}
for (index, operator) in params.new_operators.iter().enumerate() {
if operator.public_key.starts_with("<FILL_IN") {
errors.push(AppConfigValidationError::OperatorPublicKeyPlaceholder {
set: prefix,
index,
placeholder: operator.public_key.clone(),
});
}
}
}
#[derive(Debug, Clone, Error)]
pub enum AppConfigValidationError {
#[error("{field} contains placeholder value {placeholder}")]
Placeholder {
field: &'static str,
placeholder: String,
},
#[error("{set}.newOperators[{index}].publicKey contains placeholder value {placeholder}")]
OperatorPublicKeyPlaceholder {
set: &'static str,
index: usize,
placeholder: String,
},
#[error("Cannot specify both {first} and {second}")]
ConflictingFields {
first: &'static str,
second: &'static str,
},
#[error("Must specify either {first} or {second}")]
MissingOneOf {
first: &'static str,
second: &'static str,
},
#[error("{field} must be >= {minimum}, got {actual}")]
ThresholdTooLow {
field: &'static str,
minimum: u32,
actual: u32,
},
}
#[derive(Debug)]
pub struct AppConfigValidationErrors(Vec<AppConfigValidationError>);
impl AppConfigValidationErrors {
fn ok_or_errors(errors: Vec<AppConfigValidationError>) -> Result<(), Self> {
if errors.is_empty() {
return Ok(());
}
Err(Self(errors))
}
pub fn has_non_placeholder_error(&self) -> bool {
self.0.iter().any(|e| !e.is_placeholder())
}
}
impl AppConfigValidationError {
pub fn is_placeholder(&self) -> bool {
matches!(
self,
Self::Placeholder { .. } | Self::OperatorPublicKeyPlaceholder { .. }
)
}
}
impl Display for AppConfigValidationErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self
.0
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ");
Display::fmt(&s, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn valid_config_json() -> serde_json::Value {
json!({
"name": "test-app",
"quorumPublicKey": KNOWN_QUORUM_KEY,
"manifestSetParams": {
"name": "manifest-set",
"threshold": 1,
"newOperators": [{
"name": "operator-1",
"publicKey": "operator-public-key"
}]
}
})
}
#[test]
fn validate_accepts_omitted_share_set_params() {
let config: AppConfig = serde_json::from_value(valid_config_json()).unwrap();
config.validate().unwrap();
assert_eq!(config.effective_share_set_params().unwrap().threshold, 2);
}
#[test]
fn config_deserializes_enable_egress() {
let mut json = valid_config_json();
json["enableEgress"] = json!(true);
let config: AppConfig = serde_json::from_value(json).unwrap();
assert!(config.enable_egress);
}
#[test]
fn config_defaults_enable_egress_to_false() {
let config: AppConfig = serde_json::from_value(valid_config_json()).unwrap();
assert!(!config.enable_egress);
}
#[test]
fn validate_accepts_share_set_id() {
let mut json = valid_config_json();
json["shareSetId"] = json!("share-set-id");
let config: AppConfig = serde_json::from_value(json).unwrap();
config.validate().unwrap();
assert_eq!(config.share_set_id.as_deref(), Some("share-set-id"));
assert!(config.effective_share_set_params().is_none());
}
#[test]
fn validate_rejects_share_set_id_and_params() {
let mut json = valid_config_json();
json["shareSetId"] = json!("share-set-id");
json["shareSetParams"] = json!({
"name": "custom-share-set",
"threshold": 2,
"newOperators": []
});
let config: AppConfig = serde_json::from_value(json).unwrap();
assert!(
config
.validate()
.unwrap_err()
.to_string()
.contains("Cannot specify both shareSetId and shareSetParams")
);
}
#[test]
fn validate_rejects_low_share_set_threshold() {
let mut json = valid_config_json();
json["shareSetParams"] = json!({
"name": "custom-share-set",
"threshold": 1,
"newOperators": []
});
let config: AppConfig = serde_json::from_value(json).unwrap();
assert!(
config
.validate()
.unwrap_err()
.to_string()
.contains("shareSetParams.threshold must be")
);
}
}