Skip to content
Merged
1 change: 1 addition & 0 deletions .github/badges/coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schemaVersion": 1, "label": "coverage", "message": "86.2%", "color": "green"}
53 changes: 43 additions & 10 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@ enum Commands {
#[derive(Subcommand)]
enum ConfigCommands {
/// Validate configuration file (fast, no RPC calls)
Validate,
Validate {
/// Path to signers configuration file (optional)
#[arg(long)]
signers_config: Option<std::path::PathBuf>,
},
/// Validate configuration file with RPC validation (slower but more thorough)
ValidateWithRpc,
ValidateWithRpc {
/// Path to signers configuration file (optional)
#[arg(long)]
signers_config: Option<std::path::PathBuf>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -116,22 +124,47 @@ async fn main() -> Result<(), KoraError> {
match cli.command {
Some(Commands::Config { config_command }) => {
match config_command {
ConfigCommands::Validate => {
let _ = ConfigValidator::validate_with_result(rpc_client.as_ref(), true).await;
ConfigCommands::Validate { signers_config } => {
let _ = ConfigValidator::validate_with_result_and_signers(
rpc_client.as_ref(),
true,
signers_config.as_ref(),
)
.await;
}
ConfigCommands::ValidateWithRpc => {
let _ = ConfigValidator::validate_with_result(rpc_client.as_ref(), false).await;
ConfigCommands::ValidateWithRpc { signers_config } => {
let _ = ConfigValidator::validate_with_result_and_signers(
rpc_client.as_ref(),
false,
signers_config.as_ref(),
)
.await;
}
}
std::process::exit(0);
}
Some(Commands::Rpc { rpc_command }) => {
match rpc_command {
RpcCommands::Start { rpc_args } => {
// Validate config before starting server
if let Err(e) = ConfigValidator::validate(rpc_client.as_ref()).await {
print_error(&format!("Config validation failed: {e}"));
std::process::exit(1);
// Validate config and signers before starting server
match ConfigValidator::validate_with_result_and_signers(
rpc_client.as_ref(),
true,
rpc_args.signers_config.as_ref(),
)
.await
{
Err(errors) => {
for e in errors {
print_error(&format!("Validation error: {e}"));
}
std::process::exit(1);
}
Ok(warnings) => {
for w in warnings {
println!("Warning: {w}");
}
}
}

setup_logging(&rpc_args.logging_format);
Expand Down
25 changes: 24 additions & 1 deletion crates/lib/src/signer/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,14 @@ impl SignerPoolConfig {
signer.validate_individual_signer_config(index)?;
}

// Check for duplicate names
self.validate_signer_names()?;

self.validate_strategy_weights()?;

Ok(())
}

fn validate_signer_names(&self) -> Result<(), KoraError> {
let mut names = std::collections::HashSet::new();
for signer in &self.signers {
if !names.insert(&signer.name) {
Expand All @@ -120,7 +127,22 @@ impl SignerPoolConfig {
)));
}
}
Ok(())
}

fn validate_strategy_weights(&self) -> Result<(), KoraError> {
if matches!(self.signer_pool.strategy, SelectionStrategy::Weighted) {
for signer in &self.signers {
if let Some(weight) = signer.weight {
if weight == 0 {
return Err(KoraError::ValidationError(format!(
"Signer '{}' has weight of 0 in weighted strategy",
signer.name
)));
}
}
}
}
Ok(())
}
}
Expand Down Expand Up @@ -230,6 +252,7 @@ weight = 2
};

assert!(config.validate_signer_config().is_ok());
assert!(config.validate_strategy_weights().is_ok());
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/lib/src/signer/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::env;
use crate::KoraError;

pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, anyhow::Error> {
if hex.len() % 2 != 0 {
if !hex.len().is_multiple_of(2) {
return Err(anyhow::anyhow!("Hex string must have even length"));
}

Expand Down
50 changes: 46 additions & 4 deletions crates/lib/src/validator/config_validator.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use std::str::FromStr;
use std::{path::Path, str::FromStr};

use crate::{
admin::token_util::find_missing_atas,
config::Token2022Config,
config::{SplTokenConfig, Token2022Config},
fee::price::PriceModel,
oracle::PriceSource,
signer::SignerPoolConfig,
state::get_config,
token::{spl_token_2022_util, token::TokenUtil},
validator::account_validator::{validate_account, AccountType},
validator::{
account_validator::{validate_account, AccountType},
signer_validator::SignerValidator,
},
KoraError,
};
use solana_client::nonblocking::rpc_client::RpcClient;
Expand Down Expand Up @@ -41,6 +45,14 @@ impl ConfigValidator {
pub async fn validate_with_result(
rpc_client: &RpcClient,
skip_rpc_validation: bool,
) -> Result<Vec<String>, Vec<String>> {
Self::validate_with_result_and_signers(rpc_client, skip_rpc_validation, None::<&Path>).await
}

pub async fn validate_with_result_and_signers<P: AsRef<Path>>(
rpc_client: &RpcClient,
skip_rpc_validation: bool,
signers_config_path: Option<P>,
) -> Result<Vec<String>, Vec<String>> {
let mut errors = Vec::new();
let mut warnings = Vec::new();
Expand Down Expand Up @@ -119,6 +131,15 @@ impl ConfigValidator {
errors.push(format!("Invalid spl paid token address: {e}"));
}

// Warn if using "All" for allowed_spl_paid_tokens
if matches!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All) {
warnings.push(
"⚠️ Using 'All' for allowed_spl_paid_tokens - this accepts ANY SPL token for payment. \
Consider using an explicit allowlist to reduce volatility risk and protect against \
potentially malicious or worthless tokens being used for fees.".to_string()
);
}

// Validate disallowed accounts
if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
errors.push(format!("Invalid disallowed account address: {e}"));
Expand Down Expand Up @@ -236,6 +257,23 @@ impl ConfigValidator {
}
}

// Validate signers configuration if provided
if let Some(path) = signers_config_path {
match SignerPoolConfig::load_config(path.as_ref()) {
Ok(signer_config) => {
let (signer_warnings, signer_errors) =
SignerValidator::validate_with_result(&signer_config);
warnings.extend(signer_warnings);
errors.extend(signer_errors);
}
Err(e) => {
errors.push(format!("Failed to load signers config: {e}"));
}
}
} else {
println!("ℹ️ Signers configuration not validated. Include --signers-config path/to/signers.toml to validate signers");
}

// Output results
println!("=== Configuration Validation ===");
if errors.is_empty() {
Expand Down Expand Up @@ -680,11 +718,15 @@ mod tests {

let _ = update_config(config);

let mock_account = create_mock_program_account();
let rpc_client = RpcMockBuilder::new().build();

let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());

// Check that it warns about using "All" for allowed_spl_paid_tokens
let warnings = result.unwrap();
assert!(warnings.iter().any(|w| w.contains("Using 'All' for allowed_spl_paid_tokens")));
assert!(warnings.iter().any(|w| w.contains("volatility risk")));
}

#[tokio::test]
Expand Down
1 change: 1 addition & 0 deletions crates/lib/src/validator/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod account_validator;
pub mod config_validator;
pub mod signer_validator;
pub mod transaction_validator;
Loading