use std::{path::Path, str::FromStr};
use crate::{
admin::token_util::find_missing_atas,
config::{FeePayerPolicy, 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},
cache_validator::CacheValidator,
signer_validator::SignerValidator,
},
KoraError,
};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{account::Account, pubkey::Pubkey};
use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
use spl_token_2022_interface::{
extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions},
state::Mint as Token2022MintState,
ID as TOKEN_2022_PROGRAM_ID,
};
use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID;
pub struct ConfigValidator {}
impl ConfigValidator {
async fn check_token_mint_extensions(
rpc_client: &RpcClient,
allowed_tokens: &[String],
warnings: &mut Vec<String>,
) {
for token_str in allowed_tokens {
let token_pubkey = match Pubkey::from_str(token_str) {
Ok(pk) => pk,
Err(_) => continue, };
let account: Account = match rpc_client.get_account(&token_pubkey).await {
Ok(acc) => acc,
Err(_) => continue, };
if account.owner != TOKEN_2022_PROGRAM_ID {
continue;
}
let mint_with_extensions =
match StateWithExtensions::<Token2022MintState>::unpack(&account.data) {
Ok(m) => m,
Err(_) => continue, };
if mint_with_extensions
.get_extension::<spl_token_2022_interface::extension::permanent_delegate::PermanentDelegate>()
.is_ok()
{
warnings.push(format!(
"⚠️ SECURITY: Token {} has PermanentDelegate extension. \
Risk: The permanent delegate can transfer or burn tokens at any time without owner approval. \
This creates significant risks for payment tokens as funds can be seized after payment. \
Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
token_str
));
}
if mint_with_extensions
.get_extension::<spl_token_2022_interface::extension::transfer_hook::TransferHook>()
.is_ok()
{
warnings.push(format!(
"⚠️ SECURITY: Token {} has TransferHook extension. \
Risk: A custom program executes on every transfer which can reject transfers \
or introduce external dependencies and attack surface. \
Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
token_str
));
}
}
}
fn validate_fee_payer_policy(policy: &FeePayerPolicy, warnings: &mut Vec<String>) {
macro_rules! check_fee_payer_policy {
($($category:ident, $field:ident, $description:expr, $risk:expr);* $(;)?) => {
$(
if policy.$category.$field {
warnings.push(format!(
"⚠️ SECURITY: Fee payer policy allows {} ({}). \
Risk: {}. \
Consider setting [validation.fee_payer_policy.{}] {}=false to prevent abuse.",
$description,
stringify!($field),
$risk,
stringify!($category),
stringify!($field)
));
}
)*
};
}
check_fee_payer_policy! {
system, allow_transfer, "System transfers",
"Users can make the fee payer transfer arbitrary SOL amounts. This can drain your fee payer account";
system, allow_assign, "System Assign instructions",
"Users can make the fee payer reassign ownership of its accounts. This can compromise account control";
system, allow_create_account, "System CreateAccount instructions",
"Users can make the fee payer pay for arbitrary account creations. This can drain your fee payer account";
system, allow_allocate, "System Allocate instructions",
"Users can make the fee payer allocate space for accounts. This can be used to waste resources";
spl_token, allow_transfer, "SPL Token transfers",
"Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
spl_token, allow_burn, "SPL Token burn operations",
"Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
spl_token, allow_close_account, "SPL Token CloseAccount instructions",
"Users can make the fee payer close token accounts. This can disrupt operations and drain fee payer";
spl_token, allow_approve, "SPL Token approve operations",
"Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
spl_token, allow_revoke, "SPL Token revoke operations",
"Users can make the fee payer revoke delegates. This can disrupt authorized operations";
spl_token, allow_set_authority, "SPL Token SetAuthority instructions",
"Users can make the fee payer transfer authority. This can lead to complete loss of control";
spl_token, allow_mint_to, "SPL Token MintTo operations",
"Users can make the fee payer mint tokens. This can inflate token supply";
spl_token, allow_initialize_mint, "SPL Token InitializeMint instructions",
"Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
spl_token, allow_initialize_account, "SPL Token InitializeAccount instructions",
"Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
spl_token, allow_initialize_multisig, "SPL Token InitializeMultisig instructions",
"Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
spl_token, allow_freeze_account, "SPL Token FreezeAccount instructions",
"Users can make the fee payer freeze token accounts. This can disrupt token operations";
spl_token, allow_thaw_account, "SPL Token ThawAccount instructions",
"Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
token_2022, allow_transfer, "Token2022 transfers",
"Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
token_2022, allow_burn, "Token2022 burn operations",
"Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
token_2022, allow_close_account, "Token2022 CloseAccount instructions",
"Users can make the fee payer close token accounts. This can disrupt operations";
token_2022, allow_approve, "Token2022 approve operations",
"Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
token_2022, allow_revoke, "Token2022 revoke operations",
"Users can make the fee payer revoke delegates. This can disrupt authorized operations";
token_2022, allow_set_authority, "Token2022 SetAuthority instructions",
"Users can make the fee payer transfer authority. This can lead to complete loss of control";
token_2022, allow_mint_to, "Token2022 MintTo operations",
"Users can make the fee payer mint tokens. This can inflate token supply";
token_2022, allow_initialize_mint, "Token2022 InitializeMint instructions",
"Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
token_2022, allow_initialize_account, "Token2022 InitializeAccount instructions",
"Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
token_2022, allow_initialize_multisig, "Token2022 InitializeMultisig instructions",
"Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
token_2022, allow_freeze_account, "Token2022 FreezeAccount instructions",
"Users can make the fee payer freeze token accounts. This can disrupt token operations";
token_2022, allow_thaw_account, "Token2022 ThawAccount instructions",
"Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
}
macro_rules! check_nonce_policy {
($($field:ident, $description:expr, $risk:expr);* $(;)?) => {
$(
if policy.system.nonce.$field {
warnings.push(format!(
"⚠️ SECURITY: Fee payer policy allows {} (nonce.{}). \
Risk: {}. \
Consider setting [validation.fee_payer_policy.system.nonce] {}=false to prevent abuse.",
$description,
stringify!($field),
$risk,
stringify!($field)
));
}
)*
};
}
check_nonce_policy! {
allow_initialize, "nonce account initialization",
"Users can make the fee payer the authority of nonce accounts. This can create unexpected control relationships";
allow_advance, "nonce account advancement",
"Users can make the fee payer advance nonce accounts. This can be used to manipulate nonce states";
allow_withdraw, "nonce account withdrawals",
"Users can make the fee payer withdraw from nonce accounts. This can drain nonce account balances";
allow_authorize, "nonce authority changes",
"Users can make the fee payer transfer nonce authority. This can lead to loss of control over nonce accounts";
}
}
pub async fn validate(_rpc_client: &RpcClient) -> Result<(), KoraError> {
let config = &get_config()?;
if config.validation.allowed_tokens.is_empty() {
return Err(KoraError::InternalServerError("No tokens enabled".to_string()));
}
TokenUtil::check_valid_tokens(&config.validation.allowed_tokens)?;
if let Some(payment_address) = &config.kora.payment_address {
if let Err(e) = Pubkey::from_str(payment_address) {
return Err(KoraError::InternalServerError(format!(
"Invalid payment address: {e}"
)));
}
}
Ok(())
}
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
}
}
impl ConfigValidator {
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();
let config = match get_config() {
Ok(c) => c,
Err(e) => {
errors.push(format!("Failed to get config: {e}"));
return Err(errors);
}
};
if config.kora.rate_limit == 0 {
warnings.push("Rate limit is set to 0 - this will block all requests".to_string());
}
if let Some(payment_address) = &config.kora.payment_address {
if let Err(e) = Pubkey::from_str(payment_address) {
errors.push(format!("Invalid payment address: {e}"));
}
}
let methods = &config.kora.enabled_methods;
if !methods.iter().any(|enabled| enabled) {
warnings.push(
"All rpc methods are disabled - this will block all functionality".to_string(),
);
}
if config.validation.max_allowed_lamports == 0 {
warnings
.push("Max allowed lamports is 0 - this will block all SOL transfers".to_string());
}
if config.validation.max_signatures == 0 {
warnings.push("Max signatures is 0 - this will block all transactions".to_string());
}
if matches!(config.validation.price_source, PriceSource::Mock) {
warnings.push("Using Mock price source - not suitable for production".to_string());
}
if matches!(config.validation.price_source, PriceSource::Jupiter)
&& std::env::var("JUPITER_API_KEY").is_err()
{
errors.push(
"JUPITER_API_KEY environment variable not set. Required when price_source = Jupiter".to_string()
);
}
if config.validation.allow_durable_transactions {
warnings.push(
"⚠️ SECURITY: allow_durable_transactions is enabled. \
Risk: Users can hold signed transactions indefinitely and execute them much later. \
Token values may change, your fee payer may run low on funds, or you may no longer \
want to subsidize these transactions. \
Consider disabling durable transactions unless specifically required."
.to_string(),
);
}
if config.validation.allowed_programs.is_empty() {
warnings.push(
"No allowed programs configured - this will block all transactions".to_string(),
);
} else {
if !config.validation.allowed_programs.contains(&SYSTEM_PROGRAM_ID.to_string()) {
warnings.push("Missing System Program in allowed programs - SOL transfers and account operations will be blocked".to_string());
}
if !config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string())
&& !config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string())
{
warnings.push("Missing Token Program in allowed programs - SPL token operations will be blocked".to_string());
}
}
if config.validation.allowed_tokens.is_empty() {
errors.push("No allowed tokens configured".to_string());
} else if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.allowed_tokens) {
errors.push(format!("Invalid token address: {e}"));
}
if let Err(e) =
TokenUtil::check_valid_tokens(config.validation.allowed_spl_paid_tokens.as_slice())
{
errors.push(format!("Invalid spl paid token address: {e}"));
}
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()
);
}
if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
errors.push(format!("Invalid disallowed account address: {e}"));
}
if let Err(e) = validate_token2022_extensions(&config.validation.token_2022) {
errors.push(format!("Token2022 extension validation failed: {e}"));
}
if !config.validation.token_2022.is_mint_extension_blocked(ExtensionType::PermanentDelegate)
{
warnings.push(
"⚠️ SECURITY: PermanentDelegate extension is NOT blocked. Tokens with this extension \
allow the delegate to transfer/burn tokens at any time without owner approval. \
This creates significant risks:\n\
- Payment tokens: Funds can be seized after payment\n\
Consider adding \"permanent_delegate\" to blocked_mint_extensions in [validation.token2022] \
unless explicitly needed for your use case.".to_string()
);
}
let fees_enabled = !matches!(config.validation.price.model, PriceModel::Free);
if fees_enabled {
let has_token_program =
config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string());
let has_token22_program =
config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string());
if !has_token_program && !has_token22_program {
errors.push("When fees are enabled, at least one token program (SPL Token or Token2022) must be in allowed_programs".to_string());
}
if !config.validation.allowed_spl_paid_tokens.has_tokens() {
errors.push(
"When fees are enabled, allowed_spl_paid_tokens cannot be empty".to_string(),
);
}
} else {
warnings.push(
"⚠️ SECURITY: Free pricing model enabled - all transactions will be processed \
without charging fees."
.to_string(),
);
}
for paid_token in &config.validation.allowed_spl_paid_tokens {
if !config.validation.allowed_tokens.contains(paid_token) {
errors.push(format!(
"Token {paid_token} in allowed_spl_paid_tokens must also be in allowed_tokens"
));
}
}
Self::validate_fee_payer_policy(&config.validation.fee_payer_policy, &mut warnings);
match &config.validation.price.model {
PriceModel::Fixed { amount, token, strict } => {
if *amount == 0 {
warnings
.push("Fixed price amount is 0 - transactions will be free".to_string());
}
if Pubkey::from_str(token).is_err() {
errors.push(format!("Invalid token address for fixed price: {token}"));
}
if !config.validation.supports_token(token) {
errors.push(format!(
"Token address for fixed price is not in allowed spl paid tokens: {token}"
));
}
let has_auth =
config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
if !has_auth {
warnings.push(
"⚠️ SECURITY: Fixed pricing with NO authentication enabled. \
Without authentication, anyone can spam transactions at your expense. \
Consider enabling api_key or hmac_secret in [kora.auth]."
.to_string(),
);
}
if *strict {
warnings.push(
"Strict pricing mode enabled. \
Transactions where fee payer outflow exceeds the fixed price will be rejected."
.to_string(),
);
}
}
PriceModel::Margin { margin } => {
if *margin < 0.0 {
errors.push("Margin cannot be negative".to_string());
} else if *margin > 1.0 {
warnings.push(format!("Margin is {}% - this is very high", margin * 100.0));
}
}
_ => {}
};
let has_auth = config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
if !has_auth {
warnings.push(
"⚠️ SECURITY: No authentication configured (neither api_key nor hmac_secret). \
Authentication is strongly recommended for production deployments. \
Consider enabling api_key or hmac_secret in [kora.auth]."
.to_string(),
);
}
let usage_config = &config.kora.usage_limit;
if usage_config.enabled {
let (usage_errors, usage_warnings) = CacheValidator::validate(usage_config).await;
errors.extend(usage_errors);
warnings.extend(usage_warnings);
}
if !skip_rpc_validation {
for program_str in &config.validation.allowed_programs {
if let Ok(program_pubkey) = Pubkey::from_str(program_str) {
if let Err(e) =
validate_account(rpc_client, &program_pubkey, Some(AccountType::Program))
.await
{
errors.push(format!("Program {program_str} validation failed: {e}"));
}
}
}
for token_str in &config.validation.allowed_tokens {
if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
if let Err(e) =
validate_account(rpc_client, &token_pubkey, Some(AccountType::Mint)).await
{
errors.push(format!("Token {token_str} validation failed: {e}"));
}
}
}
for token_str in &config.validation.allowed_spl_paid_tokens {
if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
if let Err(e) =
validate_account(rpc_client, &token_pubkey, Some(AccountType::Mint)).await
{
errors.push(format!("SPL paid token {token_str} validation failed: {e}"));
}
}
}
Self::check_token_mint_extensions(
rpc_client,
&config.validation.allowed_tokens,
&mut warnings,
)
.await;
if let Some(payment_address) = &config.kora.payment_address {
if let Ok(payment_address) = Pubkey::from_str(payment_address) {
match find_missing_atas(rpc_client, &payment_address).await {
Ok(atas_to_create) => {
if !atas_to_create.is_empty() {
errors.push(format!(
"Missing ATAs for payment address: {payment_address}"
));
}
}
Err(e) => errors.push(format!("Failed to find missing ATAs: {e}")),
}
} else {
errors.push(format!("Invalid payment address: {payment_address}"));
}
}
}
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");
}
println!("=== Configuration Validation ===");
if errors.is_empty() {
println!("✓ Configuration validation successful!");
} else {
println!("✗ Configuration validation failed!");
println!("\n❌ Errors:");
for error in &errors {
println!(" - {error}");
}
println!("\nPlease fix the configuration errors above before deploying.");
}
if !warnings.is_empty() {
println!("\n⚠️ Warnings:");
for warning in &warnings {
println!(" - {warning}");
}
}
if errors.is_empty() {
Ok(warnings)
} else {
Err(errors)
}
}
}
fn validate_token2022_extensions(config: &Token2022Config) -> Result<(), String> {
for ext_name in &config.blocked_mint_extensions {
if spl_token_2022_util::parse_mint_extension_string(ext_name).is_none() {
return Err(format!(
"Invalid mint extension name: '{ext_name}'. Valid names are: {:?}",
spl_token_2022_util::get_all_mint_extension_names()
));
}
}
for ext_name in &config.blocked_account_extensions {
if spl_token_2022_util::parse_account_extension_string(ext_name).is_none() {
return Err(format!(
"Invalid account extension name: '{ext_name}'. Valid names are: {:?}",
spl_token_2022_util::get_all_account_extension_names()
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::{
config::{
AuthConfig, CacheConfig, Config, EnabledMethods, FeePayerPolicy, KoraConfig,
MetricsConfig, NonceInstructionPolicy, SplTokenConfig, SplTokenInstructionPolicy,
SystemInstructionPolicy, Token2022InstructionPolicy, UsageLimitConfig,
ValidationConfig,
},
constant::DEFAULT_MAX_REQUEST_BODY_SIZE,
fee::price::PriceConfig,
state::update_config,
tests::{
account_mock::create_mock_token2022_mint_with_extensions,
common::{
create_mock_non_executable_account, create_mock_program_account,
create_mock_rpc_client_account_not_found, create_mock_rpc_client_with_account,
create_mock_rpc_client_with_mint, RpcMockBuilder,
},
config_mock::ConfigMockBuilder,
},
};
use serial_test::serial;
use solana_commitment_config::CommitmentConfig;
use spl_token_2022_interface::extension::ExtensionType;
use super::*;
#[tokio::test]
#[serial]
async fn test_validate_config() {
let mut config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1000000000,
max_signatures: 10,
allowed_programs: vec!["program1".to_string()],
allowed_tokens: vec!["token1".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec!["token3".to_string()]),
disallowed_accounts: vec!["account1".to_string()],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig::default(),
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
kora: KoraConfig::default(),
metrics: MetricsConfig::default(),
};
let _ = update_config(config.clone());
config.validation.allowed_tokens = vec![];
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate(&rpc_client).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), KoraError::InternalServerError(_)));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_successful_config() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig::default(),
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
kora: KoraConfig::default(),
metrics: MetricsConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert_eq!(warnings.len(), 2);
assert!(warnings.iter().any(|w| w.contains("PermanentDelegate")));
assert!(warnings.iter().any(|w| w.contains("No authentication configured")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_warnings() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 0, max_signatures: 0, allowed_programs: vec![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Mock, fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
kora: KoraConfig {
rate_limit: 0, max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
enabled_methods: EnabledMethods {
liveness: false,
estimate_transaction_fee: false,
get_supported_tokens: false,
sign_transaction: false,
sign_and_send_transaction: false,
transfer_transaction: false,
get_blockhash: false,
get_config: false,
get_payer_signer: false,
},
auth: AuthConfig::default(),
payment_address: None,
cache: CacheConfig::default(),
usage_limit: UsageLimitConfig::default(),
},
metrics: MetricsConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(!warnings.is_empty());
assert!(warnings.iter().any(|w| w.contains("Rate limit is set to 0")));
assert!(warnings.iter().any(|w| w.contains("All rpc methods are disabled")));
assert!(warnings.iter().any(|w| w.contains("Max allowed lamports is 0")));
assert!(warnings.iter().any(|w| w.contains("Max signatures is 0")));
assert!(warnings.iter().any(|w| w.contains("Using Mock price source")));
assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_missing_system_program_warning() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec!["SomeOtherProgram".to_string()], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
kora: KoraConfig::default(),
metrics: MetricsConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(warnings.iter().any(|w| w.contains("Missing System Program in allowed programs")));
assert!(warnings.iter().any(|w| w.contains("Missing Token Program in allowed programs")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_errors() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec![], allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"invalid_token_address".to_string()
]), disallowed_accounts: vec!["invalid_account_address".to_string()], price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig {
model: PriceModel::Margin { margin: -0.1 }, },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("No allowed tokens configured")));
assert!(errors.iter().any(|e| e.contains("Invalid spl paid token address")));
assert!(errors.iter().any(|e| e.contains("Invalid disallowed account address")));
assert!(errors.iter().any(|e| e.contains("Margin cannot be negative")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_fixed_price_errors() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig {
model: PriceModel::Fixed {
amount: 0, token: "invalid_token_address".to_string(), strict: false,
},
},
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Invalid token address for fixed price")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_fixed_price_not_in_allowed_tokens() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig {
model: PriceModel::Fixed {
amount: 1000,
token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), strict: false,
},
},
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors
.iter()
.any(|e| e
.contains("Token address for fixed price is not in allowed spl paid tokens"))
);
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_fixed_price_zero_amount_warning() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig {
model: PriceModel::Fixed {
amount: 0, token: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
strict: false,
},
},
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(warnings
.iter()
.any(|w| w.contains("Fixed price amount is 0 - transactions will be free")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_fee_validation_errors() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("When fees are enabled, at least one token program (SPL Token or Token2022) must be in allowed_programs")));
assert!(errors
.iter()
.any(|e| e.contains("When fees are enabled, allowed_spl_paid_tokens cannot be empty")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_fee_and_any_spl_token_allowed() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::All, disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcMockBuilder::new().build();
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
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]
#[serial]
async fn test_validate_with_result_paid_tokens_not_in_allowed_tokens() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), ]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcMockBuilder::new().build();
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Token EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v in allowed_spl_paid_tokens must also be in allowed_tokens")));
}
fn create_program_only_config() -> Config {
Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()], allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
}
}
fn create_token_only_config() -> Config {
Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
}
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_rpc_validation_valid_program() {
let config = create_program_only_config();
let _ = update_config(config);
let rpc_client = create_mock_rpc_client_with_account(&create_mock_program_account());
let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Token")
&& e.contains("validation failed")
&& e.contains("not found")));
assert!(!errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_rpc_validation_valid_token_mint() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = create_token_only_config();
let _ = update_config(config);
let rpc_client = create_mock_rpc_client_with_mint(6);
let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_rpc_validation_non_executable_program_fails() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = create_mock_rpc_client_with_account(&create_mock_non_executable_account());
let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_rpc_validation_account_not_found_fails() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = create_mock_rpc_client_account_not_found();
let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.len() >= 2, "Should have validation errors for programs and tokens");
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_skip_rpc_validation() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = create_mock_rpc_client_account_not_found();
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok()); }
#[tokio::test]
#[serial]
async fn test_validate_with_result_valid_token2022_extensions() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: {
let mut config = Token2022Config::default();
config.blocked_mint_extensions =
vec!["transfer_fee_config".to_string(), "pausable".to_string()];
config.blocked_account_extensions =
vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
config
},
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_invalid_token2022_mint_extension() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: {
let mut config = Token2022Config::default();
config.blocked_mint_extensions = vec!["invalid_mint_extension".to_string()];
config
},
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
&& e.contains("Invalid mint extension name: 'invalid_mint_extension'")));
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_invalid_token2022_account_extension() {
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig { model: PriceModel::Free },
token_2022: {
let mut config = Token2022Config::default();
config.blocked_account_extensions =
vec!["invalid_account_extension".to_string()];
config
},
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
&& e.contains("Invalid account extension name: 'invalid_account_extension'")));
}
#[test]
fn test_validate_token2022_extensions_valid() {
let mut config = Token2022Config::default();
config.blocked_mint_extensions =
vec!["transfer_fee_config".to_string(), "pausable".to_string()];
config.blocked_account_extensions =
vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
let result = validate_token2022_extensions(&config);
assert!(result.is_ok());
}
#[test]
fn test_validate_token2022_extensions_invalid_mint_extension() {
let mut config = Token2022Config::default();
config.blocked_mint_extensions = vec!["invalid_extension".to_string()];
let result = validate_token2022_extensions(&config);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid mint extension name: 'invalid_extension'"));
}
#[test]
fn test_validate_token2022_extensions_invalid_account_extension() {
let mut config = Token2022Config::default();
config.blocked_account_extensions = vec!["invalid_extension".to_string()];
let result = validate_token2022_extensions(&config);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Invalid account extension name: 'invalid_extension'"));
}
#[test]
fn test_validate_token2022_extensions_empty() {
let config = Token2022Config::default();
let result = validate_token2022_extensions(&config);
assert!(result.is_ok());
}
#[tokio::test]
#[serial]
async fn test_validate_with_result_fee_payer_policy_warnings() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
TOKEN_2022_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy {
system: SystemInstructionPolicy {
allow_transfer: true,
allow_assign: true,
allow_create_account: true,
allow_allocate: true,
nonce: NonceInstructionPolicy {
allow_initialize: true,
allow_advance: true,
allow_withdraw: true,
allow_authorize: true,
},
},
spl_token: SplTokenInstructionPolicy {
allow_transfer: true,
allow_burn: true,
allow_close_account: true,
allow_approve: true,
allow_revoke: true,
allow_set_authority: true,
allow_mint_to: true,
allow_initialize_mint: true,
allow_initialize_account: true,
allow_initialize_multisig: true,
allow_freeze_account: true,
allow_thaw_account: true,
},
token_2022: Token2022InstructionPolicy {
allow_transfer: true,
allow_burn: true,
allow_close_account: true,
allow_approve: true,
allow_revoke: true,
allow_set_authority: true,
allow_mint_to: true,
allow_initialize_mint: true,
allow_initialize_account: true,
allow_initialize_multisig: true,
allow_freeze_account: true,
allow_thaw_account: true,
},
},
price: PriceConfig { model: PriceModel::Free },
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
metrics: MetricsConfig::default(),
kora: KoraConfig::default(),
};
let _ = update_config(config.clone());
let rpc_client = RpcClient::new_with_commitment(
"http://localhost:8899".to_string(),
CommitmentConfig::confirmed(),
);
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(warnings
.iter()
.any(|w| w.contains("System transfers") && w.contains("allow_transfer")));
assert!(warnings
.iter()
.any(|w| w.contains("System Assign instructions") && w.contains("allow_assign")));
assert!(warnings.iter().any(|w| w.contains("System CreateAccount instructions")
&& w.contains("allow_create_account")));
assert!(warnings
.iter()
.any(|w| w.contains("System Allocate instructions") && w.contains("allow_allocate")));
assert!(warnings
.iter()
.any(|w| w.contains("nonce account initialization") && w.contains("allow_initialize")));
assert!(warnings
.iter()
.any(|w| w.contains("nonce account advancement") && w.contains("allow_advance")));
assert!(warnings
.iter()
.any(|w| w.contains("nonce account withdrawals") && w.contains("allow_withdraw")));
assert!(warnings
.iter()
.any(|w| w.contains("nonce authority changes") && w.contains("allow_authorize")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token transfers") && w.contains("allow_transfer")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token burn operations") && w.contains("allow_burn")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token CloseAccount") && w.contains("allow_close_account")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token approve") && w.contains("allow_approve")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token revoke") && w.contains("allow_revoke")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token SetAuthority") && w.contains("allow_set_authority")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token MintTo") && w.contains("allow_mint_to")));
assert!(
warnings
.iter()
.any(|w| w.contains("SPL Token InitializeMint")
&& w.contains("allow_initialize_mint"))
);
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token InitializeAccount")
&& w.contains("allow_initialize_account")));
assert!(warnings.iter().any(|w| w.contains("SPL Token InitializeMultisig")
&& w.contains("allow_initialize_multisig")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token FreezeAccount") && w.contains("allow_freeze_account")));
assert!(warnings
.iter()
.any(|w| w.contains("SPL Token ThawAccount") && w.contains("allow_thaw_account")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 transfers") && w.contains("allow_transfer")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 burn operations") && w.contains("allow_burn")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 CloseAccount") && w.contains("allow_close_account")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 approve") && w.contains("allow_approve")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 revoke") && w.contains("allow_revoke")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 SetAuthority") && w.contains("allow_set_authority")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 MintTo") && w.contains("allow_mint_to")));
assert!(
warnings
.iter()
.any(|w| w.contains("Token2022 InitializeMint")
&& w.contains("allow_initialize_mint"))
);
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 InitializeAccount")
&& w.contains("allow_initialize_account")));
assert!(warnings.iter().any(|w| w.contains("Token2022 InitializeMultisig")
&& w.contains("allow_initialize_multisig")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 FreezeAccount") && w.contains("allow_freeze_account")));
assert!(warnings
.iter()
.any(|w| w.contains("Token2022 ThawAccount") && w.contains("allow_thaw_account")));
let fee_payer_warnings: Vec<_> =
warnings.iter().filter(|w| w.contains("Fee payer policy")).collect();
for warning in fee_payer_warnings {
assert!(warning.contains("Risk:"));
assert!(warning.contains("Consider setting"));
}
}
#[tokio::test]
#[serial]
async fn test_check_token_mint_extensions_permanent_delegate() {
let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
let mint_with_delegate =
create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::PermanentDelegate]);
let mint_pubkey = Pubkey::new_unique();
let rpc_client = create_mock_rpc_client_with_account(&mint_with_delegate);
let mut warnings = Vec::new();
ConfigValidator::check_token_mint_extensions(
&rpc_client,
&[mint_pubkey.to_string()],
&mut warnings,
)
.await;
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("PermanentDelegate extension"));
assert!(warnings[0].contains(&mint_pubkey.to_string()));
assert!(warnings[0].contains("Risk:"));
assert!(warnings[0].contains("permanent delegate can transfer or burn tokens"));
}
#[tokio::test]
#[serial]
async fn test_check_token_mint_extensions_transfer_hook() {
let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
let mint_with_hook =
create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::TransferHook]);
let mint_pubkey = Pubkey::new_unique();
let rpc_client = create_mock_rpc_client_with_account(&mint_with_hook);
let mut warnings = Vec::new();
ConfigValidator::check_token_mint_extensions(
&rpc_client,
&[mint_pubkey.to_string()],
&mut warnings,
)
.await;
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("TransferHook extension"));
assert!(warnings[0].contains(&mint_pubkey.to_string()));
assert!(warnings[0].contains("Risk:"));
assert!(warnings[0].contains("custom program executes on every transfer"));
}
#[tokio::test]
#[serial]
async fn test_check_token_mint_extensions_both() {
let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
let mint_with_both = create_mock_token2022_mint_with_extensions(
6,
vec![ExtensionType::PermanentDelegate, ExtensionType::TransferHook],
);
let mint_pubkey = Pubkey::new_unique();
let rpc_client = create_mock_rpc_client_with_account(&mint_with_both);
let mut warnings = Vec::new();
ConfigValidator::check_token_mint_extensions(
&rpc_client,
&[mint_pubkey.to_string()],
&mut warnings,
)
.await;
assert_eq!(warnings.len(), 2);
assert!(warnings.iter().any(|w| w.contains("PermanentDelegate extension")));
assert!(warnings.iter().any(|w| w.contains("TransferHook extension")));
}
#[tokio::test]
#[serial]
async fn test_check_token_mint_extensions_no_risky_extensions() {
let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
let mint_with_safe =
create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::MintCloseAuthority]);
let mint_pubkey = Pubkey::new_unique();
let rpc_client = create_mock_rpc_client_with_account(&mint_with_safe);
let mut warnings = Vec::new();
ConfigValidator::check_token_mint_extensions(
&rpc_client,
&[mint_pubkey.to_string()],
&mut warnings,
)
.await;
assert_eq!(warnings.len(), 0);
}
#[tokio::test]
#[serial]
async fn test_durable_transactions_warning_when_enabled() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig::default(),
token_2022: Token2022Config::default(),
allow_durable_transactions: true, },
kora: KoraConfig::default(),
metrics: MetricsConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcMockBuilder::new().build();
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(warnings.iter().any(|w| w.contains("allow_durable_transactions is enabled")));
assert!(warnings.iter().any(|w| w.contains("hold signed transactions indefinitely")));
}
#[tokio::test]
#[serial]
async fn test_durable_transactions_no_warning_when_disabled() {
std::env::set_var("JUPITER_API_KEY", "test-api-key");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter,
fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig::default(),
token_2022: Token2022Config::default(),
allow_durable_transactions: false, },
kora: KoraConfig::default(),
metrics: MetricsConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcMockBuilder::new().build();
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(!warnings.iter().any(|w| w.contains("allow_durable_transactions")));
}
#[tokio::test]
#[serial]
async fn test_jupiter_price_source_requires_api_key() {
std::env::remove_var("JUPITER_API_KEY");
let config = Config {
validation: ValidationConfig {
max_allowed_lamports: 1_000_000,
max_signatures: 10,
allowed_programs: vec![
SYSTEM_PROGRAM_ID.to_string(),
SPL_TOKEN_PROGRAM_ID.to_string(),
],
allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
]),
disallowed_accounts: vec![],
price_source: PriceSource::Jupiter, fee_payer_policy: FeePayerPolicy::default(),
price: PriceConfig::default(),
token_2022: Token2022Config::default(),
allow_durable_transactions: false,
},
kora: KoraConfig::default(),
metrics: MetricsConfig::default(),
};
let _ = update_config(config);
let rpc_client = RpcMockBuilder::new().build();
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("JUPITER_API_KEY")));
assert!(errors.iter().any(|e| e.contains("price_source = Jupiter")));
}
}