Skip to main content

kora_lib/validator/
config_validator.rs

1use std::{path::Path, str::FromStr};
2
3use crate::{
4    admin::token_util::find_missing_atas,
5    config::{FeePayerPolicy, SplTokenConfig, Token2022Config},
6    constant::{LIGHTHOUSE_PROGRAM_ID, MAX_RECAPTCHA_SCORE, MIN_RECAPTCHA_SCORE},
7    fee::price::PriceModel,
8    oracle::PriceSource,
9    plugin::TransactionPluginRunner,
10    signer::SignerPoolConfig,
11    state::get_config,
12    token::{spl_token_2022_util, token::TokenUtil},
13    validator::{
14        account_validator::{validate_account, AccountType},
15        cache_validator::CacheValidator,
16        signer_validator::SignerValidator,
17    },
18    KoraError,
19};
20use solana_client::nonblocking::rpc_client::RpcClient;
21use solana_sdk::{account::Account, pubkey::Pubkey};
22use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
23use spl_token_2022_interface::{
24    extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions},
25    state::Mint as Token2022MintState,
26    ID as TOKEN_2022_PROGRAM_ID,
27};
28use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID;
29
30pub struct ConfigValidator {}
31
32impl ConfigValidator {
33    /// Check Token2022 mints for risky extensions (PermanentDelegate, TransferHook)
34    async fn check_token_mint_extensions(
35        rpc_client: &RpcClient,
36        allowed_tokens: &[String],
37        warnings: &mut Vec<String>,
38    ) {
39        for token_str in allowed_tokens {
40            let token_pubkey = match Pubkey::from_str(token_str) {
41                Ok(pk) => pk,
42                Err(_) => continue, // Skip invalid pubkeys
43            };
44
45            let account: Account = match rpc_client.get_account(&token_pubkey).await {
46                Ok(acc) => acc,
47                Err(_) => continue, // Skip if can't fetch
48            };
49
50            if account.owner != TOKEN_2022_PROGRAM_ID {
51                continue;
52            }
53
54            let mint_with_extensions =
55                match StateWithExtensions::<Token2022MintState>::unpack(&account.data) {
56                    Ok(m) => m,
57                    Err(_) => continue, // Skip if can't parse
58                };
59
60            if mint_with_extensions
61                .get_extension::<spl_token_2022_interface::extension::permanent_delegate::PermanentDelegate>()
62                .is_ok()
63            {
64                warnings.push(format!(
65                    "⚠️  SECURITY: Token {} has PermanentDelegate extension. \
66                    Risk: The permanent delegate can transfer or burn tokens at any time without owner approval. \
67                    This creates significant risks for payment tokens as funds can be seized after payment. \
68                    Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
69                    token_str
70                ));
71            }
72
73            if mint_with_extensions
74                .get_extension::<spl_token_2022_interface::extension::transfer_hook::TransferHook>()
75                .is_ok()
76            {
77                warnings.push(format!(
78                    "⚠️  SECURITY: Token {} has TransferHook extension. \
79                    Risk: A custom program executes on every transfer which can reject transfers  \
80                    or introduce external dependencies and attack surface. \
81                    Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
82                    token_str
83                ));
84            }
85        }
86    }
87
88    /// Validate fee payer policy and add warnings for enabled risky operations
89    fn validate_fee_payer_policy(policy: &FeePayerPolicy, warnings: &mut Vec<String>) {
90        macro_rules! check_fee_payer_policy {
91        ($($category:ident, $field:ident, $description:expr, $risk:expr);* $(;)?) => {
92            $(
93                if policy.$category.$field {
94                    warnings.push(format!(
95                        "⚠️  SECURITY: Fee payer policy allows {} ({}). \
96                        Risk: {}. \
97                        Consider setting [validation.fee_payer_policy.{}] {}=false to prevent abuse.",
98                        $description,
99                        stringify!($field),
100                        $risk,
101                        stringify!($category),
102                        stringify!($field)
103                    ));
104                }
105            )*
106        };
107    }
108
109        check_fee_payer_policy! {
110            system, allow_transfer, "System transfers",
111                "Users can make the fee payer transfer arbitrary SOL amounts. This can drain your fee payer account";
112
113            system, allow_assign, "System Assign instructions",
114                "Users can make the fee payer reassign ownership of its accounts. This can compromise account control";
115
116            system, allow_create_account, "System CreateAccount instructions",
117                "Users can make the fee payer pay for arbitrary account creations. This can drain your fee payer account";
118
119            system, allow_allocate, "System Allocate instructions",
120                "Users can make the fee payer allocate space for accounts. This can be used to waste resources";
121
122            spl_token, allow_transfer, "SPL Token transfers",
123                "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
124
125            spl_token, allow_burn, "SPL Token burn operations",
126                "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
127
128            spl_token, allow_close_account, "SPL Token CloseAccount instructions",
129                "Users can make the fee payer close token accounts. This can disrupt operations and drain fee payer";
130
131            spl_token, allow_approve, "SPL Token approve operations",
132                "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
133
134            spl_token, allow_revoke, "SPL Token revoke operations",
135                "Users can make the fee payer revoke delegates. This can disrupt authorized operations";
136
137            spl_token, allow_set_authority, "SPL Token SetAuthority instructions",
138                "Users can make the fee payer transfer authority. This can lead to complete loss of control";
139
140            spl_token, allow_mint_to, "SPL Token MintTo operations",
141                "Users can make the fee payer mint tokens. This can inflate token supply";
142
143            spl_token, allow_initialize_mint, "SPL Token InitializeMint instructions",
144                "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
145
146            spl_token, allow_initialize_account, "SPL Token InitializeAccount instructions",
147                "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
148
149            spl_token, allow_initialize_multisig, "SPL Token InitializeMultisig instructions",
150                "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
151
152            spl_token, allow_freeze_account, "SPL Token FreezeAccount instructions",
153                "Users can make the fee payer freeze token accounts. This can disrupt token operations";
154
155            spl_token, allow_thaw_account, "SPL Token ThawAccount instructions",
156                "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
157
158            token_2022, allow_transfer, "Token2022 transfers",
159                "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
160
161            token_2022, allow_burn, "Token2022 burn operations",
162                "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
163
164            token_2022, allow_close_account, "Token2022 CloseAccount instructions",
165                "Users can make the fee payer close token accounts. This can disrupt operations";
166
167            token_2022, allow_approve, "Token2022 approve operations",
168                "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
169
170            token_2022, allow_revoke, "Token2022 revoke operations",
171                "Users can make the fee payer revoke delegates. This can disrupt authorized operations";
172
173            token_2022, allow_set_authority, "Token2022 SetAuthority instructions",
174                "Users can make the fee payer transfer authority. This can lead to complete loss of control";
175
176            token_2022, allow_mint_to, "Token2022 MintTo operations",
177                "Users can make the fee payer mint tokens. This can inflate token supply";
178
179            token_2022, allow_initialize_mint, "Token2022 InitializeMint instructions",
180                "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
181
182            token_2022, allow_initialize_account, "Token2022 InitializeAccount instructions",
183                "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
184
185            token_2022, allow_initialize_multisig, "Token2022 InitializeMultisig instructions",
186                "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
187
188            token_2022, allow_freeze_account, "Token2022 FreezeAccount instructions",
189                "Users can make the fee payer freeze token accounts. This can disrupt token operations";
190
191            token_2022, allow_thaw_account, "Token2022 ThawAccount instructions",
192                "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
193        }
194
195        // Check nonce policy separately (nested structure)
196        macro_rules! check_nonce_policy {
197        ($($field:ident, $description:expr, $risk:expr);* $(;)?) => {
198            $(
199                if policy.system.nonce.$field {
200                    warnings.push(format!(
201                        "⚠️  SECURITY: Fee payer policy allows {} (nonce.{}). \
202                        Risk: {}. \
203                        Consider setting [validation.fee_payer_policy.system.nonce] {}=false to prevent abuse.",
204                        $description,
205                        stringify!($field),
206                        $risk,
207                        stringify!($field)
208                    ));
209                }
210            )*
211        };
212    }
213
214        check_nonce_policy! {
215            allow_initialize, "nonce account initialization",
216                "Users can make the fee payer the authority of nonce accounts. This can create unexpected control relationships";
217
218            allow_advance, "nonce account advancement",
219                "Users can make the fee payer advance nonce accounts. This can be used to manipulate nonce states";
220
221            allow_withdraw, "nonce account withdrawals",
222                "Users can make the fee payer withdraw from nonce accounts. This can drain nonce account balances";
223
224            allow_authorize, "nonce authority changes",
225                "Users can make the fee payer transfer nonce authority. This can lead to loss of control over nonce accounts";
226        }
227    }
228
229    pub async fn validate(_rpc_client: &RpcClient) -> Result<(), KoraError> {
230        let config = &get_config()?;
231
232        if config.validation.allowed_tokens.is_empty() {
233            return Err(KoraError::InternalServerError("No tokens enabled".to_string()));
234        }
235
236        TokenUtil::check_valid_tokens(&config.validation.allowed_tokens)?;
237
238        if let Some(payment_address) = &config.kora.payment_address {
239            if let Err(e) = Pubkey::from_str(payment_address) {
240                return Err(KoraError::InternalServerError(format!(
241                    "Invalid payment address: {e}"
242                )));
243            }
244        }
245
246        Ok(())
247    }
248
249    pub async fn validate_with_result(
250        rpc_client: &RpcClient,
251        skip_rpc_validation: bool,
252    ) -> Result<Vec<String>, Vec<String>> {
253        Self::validate_with_result_and_signers(rpc_client, skip_rpc_validation, None::<&Path>).await
254    }
255}
256
257impl ConfigValidator {
258    pub async fn validate_with_result_and_signers<P: AsRef<Path>>(
259        rpc_client: &RpcClient,
260        skip_rpc_validation: bool,
261        signers_config_path: Option<P>,
262    ) -> Result<Vec<String>, Vec<String>> {
263        let mut errors = Vec::new();
264        let mut warnings = Vec::new();
265
266        let config = match get_config() {
267            Ok(c) => c,
268            Err(e) => {
269                errors.push(format!("Failed to get config: {e}"));
270                return Err(errors);
271            }
272        };
273
274        // Validate rate limit (warn if 0)
275        if config.kora.rate_limit == 0 {
276            warnings.push("Rate limit is set to 0 - this will block all requests".to_string());
277        }
278
279        // Validate payment address
280        if let Some(payment_address) = &config.kora.payment_address {
281            if let Err(e) = Pubkey::from_str(payment_address) {
282                errors.push(format!("Invalid payment address: {e}"));
283            }
284        }
285
286        // Validate reCAPTCHA score threshold (must be between MIN and MAX inclusive)
287        let score_threshold = config.kora.auth.recaptcha_score_threshold;
288        if !(MIN_RECAPTCHA_SCORE..=MAX_RECAPTCHA_SCORE).contains(&score_threshold) {
289            errors.push(format!(
290                "recaptcha_score_threshold must be between {MIN_RECAPTCHA_SCORE} and {MAX_RECAPTCHA_SCORE}, got: {score_threshold}"
291            ));
292        }
293
294        // Validate enabled methods (warn if all false)
295        let methods = &config.kora.enabled_methods;
296        if !methods.iter().any(|enabled| enabled) {
297            warnings.push(
298                "All rpc methods are disabled - this will block all functionality".to_string(),
299            );
300        }
301
302        let mut unique_plugins = std::collections::HashSet::new();
303        for plugin in &config.kora.plugins.enabled {
304            if !unique_plugins.insert(plugin.clone()) {
305                warnings.push(format!("Duplicate transaction plugin configured: {:?}", plugin));
306            }
307        }
308
309        let (plugin_errors, plugin_warnings) = TransactionPluginRunner::validate_config(config);
310        errors.extend(plugin_errors);
311        warnings.extend(plugin_warnings);
312
313        // Validate max allowed lamports (warn if 0)
314        if config.validation.max_allowed_lamports == 0 {
315            warnings
316                .push("Max allowed lamports is 0 - this will block all SOL transfers".to_string());
317        }
318
319        // Validate max signatures (warn if 0)
320        if config.validation.max_signatures == 0 {
321            warnings.push("Max signatures is 0 - this will block all transactions".to_string());
322        }
323
324        // Validate price source (warn if Mock)
325        if matches!(config.validation.price_source, PriceSource::Mock) {
326            warnings.push("Using Mock price source - not suitable for production".to_string());
327        }
328
329        // Validate Jupiter API key is set when using Jupiter price source
330        if matches!(config.validation.price_source, PriceSource::Jupiter)
331            && std::env::var("JUPITER_API_KEY").is_err()
332        {
333            errors.push(
334                "JUPITER_API_KEY environment variable not set. Required when price_source = Jupiter".to_string()
335            );
336        }
337
338        if config.validation.allow_durable_transactions {
339            warnings.push(
340                "⚠️  SECURITY: allow_durable_transactions is enabled. \
341                Risk: Users can hold signed transactions indefinitely and execute them much later. \
342                Token values may change, your fee payer may run low on funds, or you may no longer \
343                want to subsidize these transactions. \
344                Consider disabling durable transactions unless specifically required."
345                    .to_string(),
346            );
347        }
348
349        // Validate allowed programs (warn if empty or missing system/token programs)
350        if config.validation.allowed_programs.is_empty() {
351            warnings.push(
352                "No allowed programs configured - this will block all transactions".to_string(),
353            );
354        } else {
355            if !config.validation.allowed_programs.contains(&SYSTEM_PROGRAM_ID.to_string()) {
356                warnings.push("Missing System Program in allowed programs - SOL transfers and account operations will be blocked".to_string());
357            }
358            if !config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string())
359                && !config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string())
360            {
361                warnings.push("Missing Token Program in allowed programs - SPL token operations will be blocked".to_string());
362            }
363        }
364
365        // Validate lighthouse configuration
366        if config.kora.lighthouse.enabled {
367            let lighthouse_program = LIGHTHOUSE_PROGRAM_ID.to_string();
368            if !config.validation.allowed_programs.contains(&lighthouse_program) {
369                errors.push(format!(
370                    "Lighthouse is enabled but {} is not in allowed_programs. Consider adding it to allowed_programs.",
371                    LIGHTHOUSE_PROGRAM_ID
372                ));
373            }
374            if config.validation.disallowed_accounts.contains(&lighthouse_program) {
375                errors.push(format!(
376                    "Lighthouse is enabled but {} is in disallowed_accounts. Consider removing it from disallowed_accounts.",
377                    LIGHTHOUSE_PROGRAM_ID
378                ));
379            }
380
381            // Warn about signAndSend methods not having lighthouse protection
382            let enabled_methods = &config.kora.enabled_methods;
383            let mut unprotected_methods = Vec::new();
384            if enabled_methods.sign_and_send_transaction {
385                unprotected_methods.push("signAndSendTransaction");
386            }
387            if enabled_methods.sign_and_send_bundle {
388                unprotected_methods.push("signAndSendBundle");
389            }
390            if !unprotected_methods.is_empty() {
391                warnings.push(format!(
392                    "Lighthouse is enabled but {} will NOT have fee payer protection. \
393                    These methods send transactions directly to the network, so adding assertions \
394                    would invalidate client signatures. Consider using signTransaction/signBundle instead.",
395                    unprotected_methods.join(", ")
396                ));
397            }
398        }
399
400        // Validate base58 pubkey format for allowed_programs
401        for pubkey_str in &config.validation.allowed_programs {
402            if Pubkey::from_str(pubkey_str).is_err() {
403                errors.push(format!(
404                    "Invalid base58 pubkey format in allowed_programs: '{}'",
405                    pubkey_str
406                ));
407            }
408        }
409
410        // Validate allowed tokens
411        if config.validation.allowed_tokens.is_empty() {
412            errors.push("No allowed tokens configured".to_string());
413        } else if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.allowed_tokens) {
414            errors.push(format!("Invalid token address: {e}"));
415        }
416
417        // Validate allowed spl paid tokens
418        if let Err(e) =
419            TokenUtil::check_valid_tokens(config.validation.allowed_spl_paid_tokens.as_slice())
420        {
421            errors.push(format!("Invalid spl paid token address: {e}"));
422        }
423
424        // Warn if using "All" for allowed_spl_paid_tokens
425        if matches!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All) {
426            warnings.push(
427                "⚠️  Using 'All' for allowed_spl_paid_tokens - this accepts ANY SPL token for payment. \
428                Consider using an explicit allowlist to reduce volatility risk and protect against \
429                potentially malicious or worthless tokens being used for fees.".to_string()
430            );
431        }
432
433        // Validate disallowed accounts
434        if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
435            errors.push(format!("Invalid disallowed account address: {e}"));
436        }
437
438        // Validate Token2022 extensions
439        if let Err(e) = validate_token2022_extensions(&config.validation.token_2022) {
440            errors.push(format!("Token2022 extension validation failed: {e}"));
441        }
442
443        // Warn if PermanentDelegate is not blocked
444        if !config.validation.token_2022.is_mint_extension_blocked(ExtensionType::PermanentDelegate)
445        {
446            warnings.push(
447                "⚠️  SECURITY: PermanentDelegate extension is NOT blocked. Tokens with this extension \
448                allow the delegate to transfer/burn tokens at any time without owner approval. \
449                This creates significant risks:\n\
450                  - Payment tokens: Funds can be seized after payment\n\
451                Consider adding \"permanent_delegate\" to blocked_mint_extensions in [validation.token2022] \
452                unless explicitly needed for your use case.".to_string()
453            );
454        }
455
456        // Check if fees are enabled (not Free pricing)
457        let fees_enabled = !matches!(config.validation.price.model, PriceModel::Free);
458
459        if fees_enabled {
460            // If fees enabled, token or token22 must be enabled in allowed_programs
461            let has_token_program =
462                config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string());
463            let has_token22_program =
464                config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string());
465
466            if !has_token_program && !has_token22_program {
467                errors.push("When fees are enabled, at least one token program (SPL Token or Token2022) must be in allowed_programs".to_string());
468            }
469
470            // If fees enabled, allowed_spl_paid_tokens can't be empty
471            if !config.validation.allowed_spl_paid_tokens.has_tokens() {
472                errors.push(
473                    "When fees are enabled, allowed_spl_paid_tokens cannot be empty".to_string(),
474                );
475            }
476        } else {
477            warnings.push(
478                "⚠️  SECURITY: Free pricing model enabled - all transactions will be processed \
479                without charging fees."
480                    .to_string(),
481            );
482        }
483
484        // Validate that all tokens in allowed_spl_paid_tokens are also in allowed_tokens
485        for paid_token in &config.validation.allowed_spl_paid_tokens {
486            if !config.validation.allowed_tokens.contains(paid_token) {
487                errors.push(format!(
488                    "Token {paid_token} in allowed_spl_paid_tokens must also be in allowed_tokens"
489                ));
490            }
491        }
492
493        // Validate fee payer policy - warn about enabled risky operations
494        Self::validate_fee_payer_policy(&config.validation.fee_payer_policy, &mut warnings);
495
496        // Validate margin (error if negative)
497        match &config.validation.price.model {
498            PriceModel::Fixed { amount, token, strict } => {
499                if *amount == 0 {
500                    warnings
501                        .push("Fixed price amount is 0 - transactions will be free".to_string());
502                }
503                if Pubkey::from_str(token).is_err() {
504                    errors.push(format!("Invalid token address for fixed price: {token}"));
505                }
506                if !config.validation.supports_token(token) {
507                    errors.push(format!(
508                        "Token address for fixed price is not in allowed spl paid tokens: {token}"
509                    ));
510                }
511
512                // Warn about dangerous configurations with fixed pricing
513                let has_auth =
514                    config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
515                if !has_auth {
516                    warnings.push(
517                        "⚠️  SECURITY: Fixed pricing with NO authentication enabled. \
518                        Without authentication, anyone can spam transactions at your expense. \
519                        Consider enabling api_key or hmac_secret in [kora.auth]."
520                            .to_string(),
521                    );
522                }
523
524                // Warn about strict mode
525                if *strict {
526                    warnings.push(
527                        "Strict pricing mode enabled. \
528                        Transactions where fee payer outflow exceeds the fixed price will be rejected."
529                            .to_string(),
530                    );
531                }
532            }
533            PriceModel::Margin { margin } => {
534                if *margin < 0.0 {
535                    errors.push("Margin cannot be negative".to_string());
536                } else if *margin > 1.0 {
537                    warnings.push(format!("Margin is {}% - this is very high", margin * 100.0));
538                }
539            }
540            _ => {}
541        };
542
543        // General authentication warning
544        let has_auth = config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
545        if !has_auth {
546            warnings.push(
547                "⚠️  SECURITY: No authentication configured (neither api_key nor hmac_secret). \
548                Authentication is strongly recommended for production deployments. \
549                Consider enabling api_key or hmac_secret in [kora.auth]."
550                    .to_string(),
551            );
552        }
553
554        // Validate usage limit configuration
555        let usage_config = &config.kora.usage_limit;
556        if usage_config.enabled {
557            let (usage_errors, usage_warnings) = CacheValidator::validate(usage_config).await;
558            errors.extend(usage_errors);
559            warnings.extend(usage_warnings);
560        }
561
562        // Validate RPC cache config
563        if config.kora.cache.enabled {
564            let (cache_errors, cache_warnings) =
565                CacheValidator::validate_rpc_cache(&config.kora.cache).await;
566            errors.extend(cache_errors);
567            warnings.extend(cache_warnings);
568        }
569
570        // RPC validation - only if not skipped
571        if !skip_rpc_validation {
572            // Validate allowed programs - should be executable
573            for program_str in &config.validation.allowed_programs {
574                if let Ok(program_pubkey) = Pubkey::from_str(program_str) {
575                    if let Err(e) = validate_account(
576                        config,
577                        rpc_client,
578                        &program_pubkey,
579                        Some(AccountType::Program),
580                    )
581                    .await
582                    {
583                        errors.push(format!("Program {program_str} validation failed: {e}"));
584                    }
585                }
586            }
587
588            // Validate allowed tokens - should be non-executable token mints
589            for token_str in &config.validation.allowed_tokens {
590                if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
591                    if let Err(e) =
592                        validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
593                            .await
594                    {
595                        errors.push(format!("Token {token_str} validation failed: {e}"));
596                    }
597                }
598            }
599
600            // Validate allowed spl paid tokens - should be non-executable token mints
601            for token_str in &config.validation.allowed_spl_paid_tokens {
602                if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
603                    if let Err(e) =
604                        validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
605                            .await
606                    {
607                        errors.push(format!("SPL paid token {token_str} validation failed: {e}"));
608                    }
609                }
610            }
611
612            // Check Token2022 mints for risky extensions
613            Self::check_token_mint_extensions(
614                rpc_client,
615                &config.validation.allowed_tokens,
616                &mut warnings,
617            )
618            .await;
619
620            // Validate missing ATAs for payment address
621            if let Some(payment_address) = &config.kora.payment_address {
622                if let Ok(payment_address) = Pubkey::from_str(payment_address) {
623                    match find_missing_atas(config, rpc_client, &payment_address).await {
624                        Ok(atas_to_create) => {
625                            if !atas_to_create.is_empty() {
626                                errors.push(format!(
627                                    "Missing ATAs for payment address: {payment_address}"
628                                ));
629                            }
630                        }
631                        Err(e) => errors.push(format!("Failed to find missing ATAs: {e}")),
632                    }
633                } else {
634                    errors.push(format!("Invalid payment address: {payment_address}"));
635                }
636            }
637        }
638
639        // Validate signers configuration if provided
640        if let Some(path) = signers_config_path {
641            match SignerPoolConfig::load_config(path.as_ref()) {
642                Ok(signer_config) => {
643                    let (signer_warnings, signer_errors) =
644                        SignerValidator::validate_with_result(&signer_config);
645                    warnings.extend(signer_warnings);
646                    errors.extend(signer_errors);
647                }
648                Err(e) => {
649                    errors.push(format!("Failed to load signers config: {e}"));
650                }
651            }
652        } else {
653            println!("ℹ️  Signers configuration not validated. Include --signers-config path/to/signers.toml to validate signers");
654        }
655
656        // Output results
657        println!("=== Configuration Validation ===");
658        if errors.is_empty() {
659            println!("✓ Configuration validation successful!");
660        } else {
661            println!("✗ Configuration validation failed!");
662            println!("\n❌ Errors:");
663            for error in &errors {
664                println!("   - {error}");
665            }
666            println!("\nPlease fix the configuration errors above before deploying.");
667        }
668
669        if !warnings.is_empty() {
670            println!("\n⚠️  Warnings:");
671            for warning in &warnings {
672                println!("   - {warning}");
673            }
674        }
675
676        if errors.is_empty() {
677            Ok(warnings)
678        } else {
679            Err(errors)
680        }
681    }
682}
683
684/// Validate Token2022 extension configuration
685fn validate_token2022_extensions(config: &Token2022Config) -> Result<(), String> {
686    // Validate blocked mint extensions
687    for ext_name in &config.blocked_mint_extensions {
688        if spl_token_2022_util::parse_mint_extension_string(ext_name).is_none() {
689            return Err(format!(
690                "Invalid mint extension name: '{ext_name}'. Valid names are: {:?}",
691                spl_token_2022_util::get_all_mint_extension_names()
692            ));
693        }
694    }
695
696    // Validate blocked account extensions
697    for ext_name in &config.blocked_account_extensions {
698        if spl_token_2022_util::parse_account_extension_string(ext_name).is_none() {
699            return Err(format!(
700                "Invalid account extension name: '{ext_name}'. Valid names are: {:?}",
701                spl_token_2022_util::get_all_account_extension_names()
702            ));
703        }
704    }
705
706    Ok(())
707}
708
709#[cfg(test)]
710mod tests {
711    use crate::{
712        config::{
713            AuthConfig, BundleConfig, CacheConfig, Config, EnabledMethods, FeePayerPolicy,
714            KoraConfig, LighthouseConfig, MetricsConfig, NonceInstructionPolicy, PluginsConfig,
715            SplTokenConfig, SplTokenInstructionPolicy, SystemInstructionPolicy,
716            Token2022InstructionPolicy, TransactionPluginType, UsageLimitConfig, ValidationConfig,
717        },
718        constant::{DEFAULT_MAX_REQUEST_BODY_SIZE, LIGHTHOUSE_PROGRAM_ID},
719        fee::price::PriceConfig,
720        state::update_config,
721        tests::{
722            account_mock::create_mock_token2022_mint_with_extensions,
723            common::{
724                create_mock_non_executable_account, create_mock_program_account,
725                create_mock_rpc_client_account_not_found, create_mock_rpc_client_with_account,
726                create_mock_rpc_client_with_mint, RpcMockBuilder,
727            },
728            config_mock::ConfigMockBuilder,
729        },
730    };
731    use serial_test::serial;
732    use solana_commitment_config::CommitmentConfig;
733    use spl_token_2022_interface::extension::ExtensionType;
734
735    use super::*;
736
737    #[tokio::test]
738    #[serial]
739    async fn test_validate_config() {
740        let mut config = Config {
741            validation: ValidationConfig {
742                max_allowed_lamports: 1000000000,
743                max_signatures: 10,
744                allowed_programs: vec!["program1".to_string()],
745                allowed_tokens: vec!["token1".to_string()],
746                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec!["token3".to_string()]),
747                disallowed_accounts: vec!["account1".to_string()],
748                price_source: PriceSource::Jupiter,
749                fee_payer_policy: FeePayerPolicy::default(),
750                price: PriceConfig::default(),
751                token_2022: Token2022Config::default(),
752                allow_durable_transactions: false,
753                max_price_staleness_slots: 0,
754            },
755            kora: KoraConfig::default(),
756            metrics: MetricsConfig::default(),
757        };
758
759        // Initialize global config
760        let _ = update_config(config.clone());
761
762        // Test empty tokens list
763        config.validation.allowed_tokens = vec![];
764        let _ = update_config(config);
765
766        let rpc_client = RpcClient::new_with_commitment(
767            "http://localhost:8899".to_string(),
768            CommitmentConfig::confirmed(),
769        );
770        let result = ConfigValidator::validate(&rpc_client).await;
771        assert!(result.is_err());
772        assert!(matches!(result.unwrap_err(), KoraError::InternalServerError(_)));
773    }
774
775    #[tokio::test]
776    #[serial]
777    async fn test_validate_with_result_successful_config() {
778        std::env::set_var("JUPITER_API_KEY", "test-api-key");
779        let config = Config {
780            validation: ValidationConfig {
781                max_allowed_lamports: 1_000_000,
782                max_signatures: 10,
783                allowed_programs: vec![
784                    SYSTEM_PROGRAM_ID.to_string(),
785                    SPL_TOKEN_PROGRAM_ID.to_string(),
786                ],
787                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
788                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
789                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
790                ]),
791                disallowed_accounts: vec![],
792                price_source: PriceSource::Jupiter,
793                fee_payer_policy: FeePayerPolicy::default(),
794                price: PriceConfig::default(),
795                token_2022: Token2022Config::default(),
796                allow_durable_transactions: false,
797                max_price_staleness_slots: 0,
798            },
799            kora: KoraConfig::default(),
800            metrics: MetricsConfig::default(),
801        };
802
803        // Initialize global config
804        let _ = update_config(config);
805
806        let rpc_client = RpcClient::new_with_commitment(
807            "http://localhost:8899".to_string(),
808            CommitmentConfig::confirmed(),
809        );
810        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
811        assert!(result.is_ok());
812        let warnings = result.unwrap();
813        // Expect warnings about PermanentDelegate and no authentication
814        assert_eq!(warnings.len(), 2);
815        assert!(warnings.iter().any(|w| w.contains("PermanentDelegate")));
816        assert!(warnings.iter().any(|w| w.contains("No authentication configured")));
817    }
818
819    #[tokio::test]
820    #[serial]
821    async fn test_validate_with_result_warnings() {
822        let config = Config {
823            validation: ValidationConfig {
824                max_allowed_lamports: 0,  // Should warn
825                max_signatures: 0,        // Should warn
826                allowed_programs: vec![], // Should warn
827                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
828                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
829                disallowed_accounts: vec![],
830                price_source: PriceSource::Mock, // Should warn
831                fee_payer_policy: FeePayerPolicy::default(),
832                price: PriceConfig { model: PriceModel::Free },
833                token_2022: Token2022Config::default(),
834                allow_durable_transactions: false,
835                max_price_staleness_slots: 0,
836            },
837            kora: KoraConfig {
838                rate_limit: 0, // Should warn
839                max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
840                enabled_methods: EnabledMethods {
841                    liveness: false,
842                    estimate_transaction_fee: false,
843                    get_supported_tokens: false,
844                    sign_transaction: false,
845                    sign_and_send_transaction: false,
846                    transfer_transaction: false,
847                    get_blockhash: false,
848                    get_config: false,
849                    get_payer_signer: false,
850                    get_version: false,
851                    estimate_bundle_fee: false,
852                    sign_and_send_bundle: false,
853                    sign_bundle: false,
854                },
855                auth: AuthConfig::default(),
856                payment_address: None,
857                cache: CacheConfig::default(),
858                usage_limit: UsageLimitConfig::default(),
859                plugins: PluginsConfig::default(),
860                bundle: BundleConfig::default(),
861                lighthouse: LighthouseConfig::default(),
862                force_sig_verify: false,
863            },
864            metrics: MetricsConfig::default(),
865        };
866
867        // Initialize global config
868        let _ = update_config(config);
869
870        let rpc_client = RpcClient::new_with_commitment(
871            "http://localhost:8899".to_string(),
872            CommitmentConfig::confirmed(),
873        );
874        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
875        assert!(result.is_ok());
876        let warnings = result.unwrap();
877
878        assert!(!warnings.is_empty());
879        assert!(warnings.iter().any(|w| w.contains("Rate limit is set to 0")));
880        assert!(warnings.iter().any(|w| w.contains("All rpc methods are disabled")));
881        assert!(warnings.iter().any(|w| w.contains("Max allowed lamports is 0")));
882        assert!(warnings.iter().any(|w| w.contains("Max signatures is 0")));
883        assert!(warnings.iter().any(|w| w.contains("Using Mock price source")));
884        assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
885    }
886
887    #[tokio::test]
888    #[serial]
889    async fn test_validate_with_result_duplicate_plugins_warn() {
890        let mut config = ConfigMockBuilder::new().build();
891        config.kora.cache.enabled = false;
892        config.kora.plugins.enabled =
893            vec![TransactionPluginType::GasSwap, TransactionPluginType::GasSwap];
894
895        let _ = update_config(config);
896
897        let rpc_client = RpcMockBuilder::new().build();
898        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
899        assert!(result.is_ok());
900        let warnings = result.unwrap();
901
902        assert!(warnings.iter().any(|w| w.contains("Duplicate transaction plugin configured")));
903    }
904
905    #[tokio::test]
906    #[serial]
907    async fn test_gas_swap_plugin_requires_system_program() {
908        let mut config = ConfigMockBuilder::new().build();
909        config.kora.cache.enabled = false;
910        config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
911        config.validation.allowed_programs = vec![SPL_TOKEN_PROGRAM_ID.to_string()]; // no system program
912
913        let _ = update_config(config);
914
915        let rpc_client = RpcMockBuilder::new().build();
916        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
917        assert!(result.is_err());
918        assert!(result
919            .unwrap_err()
920            .iter()
921            .any(|e| e.contains("GasSwap plugin requires System Program")));
922    }
923
924    #[tokio::test]
925    #[serial]
926    async fn test_gas_swap_plugin_requires_token_program() {
927        let mut config = ConfigMockBuilder::new().build();
928        config.kora.cache.enabled = false;
929        config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
930        config.validation.allowed_programs = vec![SYSTEM_PROGRAM_ID.to_string()]; // no token program
931
932        let _ = update_config(config);
933
934        let rpc_client = RpcMockBuilder::new().build();
935        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
936        assert!(result.is_err());
937        assert!(result
938            .unwrap_err()
939            .iter()
940            .any(|e| e.contains("GasSwap plugin requires at least one token program")));
941    }
942
943    #[tokio::test]
944    #[serial]
945    async fn test_gas_swap_plugin_requires_allowed_tokens() {
946        let mut config = ConfigMockBuilder::new().build();
947        config.kora.cache.enabled = false;
948        config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
949        config.validation.allowed_tokens = vec![];
950
951        let _ = update_config(config);
952
953        let rpc_client = RpcMockBuilder::new().build();
954        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
955        assert!(result.is_err());
956        assert!(result
957            .unwrap_err()
958            .iter()
959            .any(|e| e.contains("GasSwap plugin requires at least one token in allowed_tokens")));
960    }
961
962    #[tokio::test]
963    #[serial]
964    async fn test_gas_swap_plugin_rejects_free_pricing() {
965        let mut config = ConfigMockBuilder::new().build();
966        config.kora.cache.enabled = false;
967        config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
968        config.validation.price = PriceConfig { model: PriceModel::Free };
969
970        let _ = update_config(config);
971
972        let rpc_client = RpcMockBuilder::new().build();
973        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
974        assert!(result.is_err());
975        assert!(result
976            .unwrap_err()
977            .iter()
978            .any(|e| e.contains("GasSwap plugin cannot be used with Free pricing")));
979    }
980
981    #[tokio::test]
982    #[serial]
983    async fn test_validate_with_result_missing_system_program_warning() {
984        std::env::set_var("JUPITER_API_KEY", "test-api-key");
985        let config = Config {
986            validation: ValidationConfig {
987                max_allowed_lamports: 1_000_000,
988                max_signatures: 10,
989                allowed_programs: vec!["11111111111111111111111111111112".to_string()], // Missing system program, but valid base58
990                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
991                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
992                disallowed_accounts: vec![],
993                price_source: PriceSource::Jupiter,
994                fee_payer_policy: FeePayerPolicy::default(),
995                price: PriceConfig { model: PriceModel::Free },
996                token_2022: Token2022Config::default(),
997                allow_durable_transactions: false,
998                max_price_staleness_slots: 0,
999            },
1000            kora: KoraConfig::default(),
1001            metrics: MetricsConfig::default(),
1002        };
1003
1004        // Initialize global config
1005        let _ = update_config(config);
1006
1007        let rpc_client = RpcClient::new_with_commitment(
1008            "http://localhost:8899".to_string(),
1009            CommitmentConfig::confirmed(),
1010        );
1011        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1012        assert!(result.is_ok());
1013        let warnings = result.unwrap();
1014
1015        assert!(warnings.iter().any(|w| w.contains("Missing System Program in allowed programs")));
1016        assert!(warnings.iter().any(|w| w.contains("Missing Token Program in allowed programs")));
1017    }
1018
1019    #[tokio::test]
1020    #[serial]
1021    async fn test_validate_with_result_errors() {
1022        let config = Config {
1023            validation: ValidationConfig {
1024                max_allowed_lamports: 1_000_000,
1025                max_signatures: 10,
1026                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1027                allowed_tokens: vec![], // Error - no tokens
1028                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1029                    "invalid_token_address".to_string()
1030                ]), // Error - invalid token
1031                disallowed_accounts: vec!["invalid_account_address".to_string()], // Error - invalid account
1032                price_source: PriceSource::Jupiter,
1033                fee_payer_policy: FeePayerPolicy::default(),
1034                price: PriceConfig {
1035                    model: PriceModel::Margin { margin: -0.1 }, // Error - negative margin
1036                },
1037                token_2022: Token2022Config::default(),
1038                allow_durable_transactions: false,
1039                max_price_staleness_slots: 0,
1040            },
1041            metrics: MetricsConfig::default(),
1042            kora: KoraConfig::default(),
1043        };
1044
1045        let _ = update_config(config);
1046
1047        let rpc_client = RpcClient::new_with_commitment(
1048            "http://localhost:8899".to_string(),
1049            CommitmentConfig::confirmed(),
1050        );
1051        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1052        assert!(result.is_err());
1053        let errors = result.unwrap_err();
1054
1055        assert!(errors.iter().any(|e| e.contains("No allowed tokens configured")));
1056        assert!(errors.iter().any(|e| e.contains("Invalid spl paid token address")));
1057        assert!(errors.iter().any(|e| e.contains("Invalid disallowed account address")));
1058        assert!(errors.iter().any(|e| e.contains("Margin cannot be negative")));
1059    }
1060
1061    #[tokio::test]
1062    #[serial]
1063    async fn test_validate_with_result_fixed_price_errors() {
1064        let config = Config {
1065            validation: ValidationConfig {
1066                max_allowed_lamports: 1_000_000,
1067                max_signatures: 10,
1068                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1069                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1070                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1071                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1072                ]),
1073                disallowed_accounts: vec![],
1074                price_source: PriceSource::Jupiter,
1075                fee_payer_policy: FeePayerPolicy::default(),
1076                price: PriceConfig {
1077                    model: PriceModel::Fixed {
1078                        amount: 0,                                  // Should warn
1079                        token: "invalid_token_address".to_string(), // Should error
1080                        strict: false,
1081                    },
1082                },
1083                token_2022: Token2022Config::default(),
1084                allow_durable_transactions: false,
1085                max_price_staleness_slots: 0,
1086            },
1087            metrics: MetricsConfig::default(),
1088            kora: KoraConfig::default(),
1089        };
1090
1091        let _ = update_config(config);
1092
1093        let rpc_client = RpcClient::new_with_commitment(
1094            "http://localhost:8899".to_string(),
1095            CommitmentConfig::confirmed(),
1096        );
1097        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1098        assert!(result.is_err());
1099        let errors = result.unwrap_err();
1100
1101        assert!(errors.iter().any(|e| e.contains("Invalid token address for fixed price")));
1102    }
1103
1104    #[tokio::test]
1105    #[serial]
1106    async fn test_validate_with_result_fixed_price_not_in_allowed_tokens() {
1107        let config = Config {
1108            validation: ValidationConfig {
1109                max_allowed_lamports: 1_000_000,
1110                max_signatures: 10,
1111                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1112                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1113                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1114                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1115                ]),
1116                disallowed_accounts: vec![],
1117                price_source: PriceSource::Jupiter,
1118                fee_payer_policy: FeePayerPolicy::default(),
1119                price: PriceConfig {
1120                    model: PriceModel::Fixed {
1121                        amount: 1000,
1122                        token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // Valid but not in allowed
1123                        strict: false,
1124                    },
1125                },
1126                token_2022: Token2022Config::default(),
1127                allow_durable_transactions: false,
1128                max_price_staleness_slots: 0,
1129            },
1130            metrics: MetricsConfig::default(),
1131            kora: KoraConfig::default(),
1132        };
1133
1134        let _ = update_config(config);
1135
1136        let rpc_client = RpcClient::new_with_commitment(
1137            "http://localhost:8899".to_string(),
1138            CommitmentConfig::confirmed(),
1139        );
1140        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1141        assert!(result.is_err());
1142        let errors = result.unwrap_err();
1143
1144        assert!(
1145            errors
1146                .iter()
1147                .any(|e| e
1148                    .contains("Token address for fixed price is not in allowed spl paid tokens"))
1149        );
1150    }
1151
1152    #[tokio::test]
1153    #[serial]
1154    async fn test_validate_with_result_fixed_price_zero_amount_warning() {
1155        std::env::set_var("JUPITER_API_KEY", "test-api-key");
1156        let config = Config {
1157            validation: ValidationConfig {
1158                max_allowed_lamports: 1_000_000,
1159                max_signatures: 10,
1160                allowed_programs: vec![
1161                    SYSTEM_PROGRAM_ID.to_string(),
1162                    SPL_TOKEN_PROGRAM_ID.to_string(),
1163                ],
1164                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1165                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1166                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1167                ]),
1168                disallowed_accounts: vec![],
1169                price_source: PriceSource::Jupiter,
1170                fee_payer_policy: FeePayerPolicy::default(),
1171                price: PriceConfig {
1172                    model: PriceModel::Fixed {
1173                        amount: 0, // Should warn
1174                        token: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1175                        strict: false,
1176                    },
1177                },
1178                token_2022: Token2022Config::default(),
1179                allow_durable_transactions: false,
1180                max_price_staleness_slots: 0,
1181            },
1182            metrics: MetricsConfig::default(),
1183            kora: KoraConfig::default(),
1184        };
1185
1186        let _ = update_config(config);
1187
1188        let rpc_client = RpcClient::new_with_commitment(
1189            "http://localhost:8899".to_string(),
1190            CommitmentConfig::confirmed(),
1191        );
1192        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1193        assert!(result.is_ok());
1194        let warnings = result.unwrap();
1195
1196        assert!(warnings
1197            .iter()
1198            .any(|w| w.contains("Fixed price amount is 0 - transactions will be free")));
1199    }
1200
1201    #[tokio::test]
1202    #[serial]
1203    async fn test_validate_with_result_fee_validation_errors() {
1204        let config = Config {
1205            validation: ValidationConfig {
1206                max_allowed_lamports: 1_000_000,
1207                max_signatures: 10,
1208                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()], // Missing token programs
1209                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1210                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), // Empty when fees enabled - should error
1211                disallowed_accounts: vec![],
1212                price_source: PriceSource::Jupiter,
1213                fee_payer_policy: FeePayerPolicy::default(),
1214                price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1215                token_2022: Token2022Config::default(),
1216                allow_durable_transactions: false,
1217                max_price_staleness_slots: 0,
1218            },
1219            metrics: MetricsConfig::default(),
1220            kora: KoraConfig::default(),
1221        };
1222
1223        let _ = update_config(config);
1224
1225        let rpc_client = RpcClient::new_with_commitment(
1226            "http://localhost:8899".to_string(),
1227            CommitmentConfig::confirmed(),
1228        );
1229        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1230        assert!(result.is_err());
1231        let errors = result.unwrap_err();
1232
1233        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")));
1234        assert!(errors
1235            .iter()
1236            .any(|e| e.contains("When fees are enabled, allowed_spl_paid_tokens cannot be empty")));
1237    }
1238
1239    #[tokio::test]
1240    #[serial]
1241    async fn test_validate_with_result_fee_and_any_spl_token_allowed() {
1242        std::env::set_var("JUPITER_API_KEY", "test-api-key");
1243        let config = Config {
1244            validation: ValidationConfig {
1245                max_allowed_lamports: 1_000_000,
1246                max_signatures: 10,
1247                allowed_programs: vec![
1248                    SYSTEM_PROGRAM_ID.to_string(),
1249                    SPL_TOKEN_PROGRAM_ID.to_string(),
1250                ],
1251                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1252                allowed_spl_paid_tokens: SplTokenConfig::All, // All tokens are allowed
1253                disallowed_accounts: vec![],
1254                price_source: PriceSource::Jupiter,
1255                fee_payer_policy: FeePayerPolicy::default(),
1256                price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1257                token_2022: Token2022Config::default(),
1258                allow_durable_transactions: false,
1259                max_price_staleness_slots: 0,
1260            },
1261            metrics: MetricsConfig::default(),
1262            kora: KoraConfig::default(),
1263        };
1264
1265        let _ = update_config(config);
1266
1267        let rpc_client = RpcMockBuilder::new().build();
1268
1269        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1270        assert!(result.is_ok());
1271
1272        // Check that it warns about using "All" for allowed_spl_paid_tokens
1273        let warnings = result.unwrap();
1274        assert!(warnings.iter().any(|w| w.contains("Using 'All' for allowed_spl_paid_tokens")));
1275        assert!(warnings.iter().any(|w| w.contains("volatility risk")));
1276    }
1277
1278    #[tokio::test]
1279    #[serial]
1280    async fn test_validate_with_result_paid_tokens_not_in_allowed_tokens() {
1281        let config = Config {
1282            validation: ValidationConfig {
1283                max_allowed_lamports: 1_000_000,
1284                max_signatures: 10,
1285                allowed_programs: vec![
1286                    SYSTEM_PROGRAM_ID.to_string(),
1287                    SPL_TOKEN_PROGRAM_ID.to_string(),
1288                ],
1289                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1290                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1291                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // Not in allowed_tokens
1292                ]),
1293                disallowed_accounts: vec![],
1294                price_source: PriceSource::Jupiter,
1295                fee_payer_policy: FeePayerPolicy::default(),
1296                price: PriceConfig { model: PriceModel::Free },
1297                token_2022: Token2022Config::default(),
1298                allow_durable_transactions: false,
1299                max_price_staleness_slots: 0,
1300            },
1301            metrics: MetricsConfig::default(),
1302            kora: KoraConfig::default(),
1303        };
1304
1305        let _ = update_config(config);
1306
1307        let rpc_client = RpcMockBuilder::new().build();
1308        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1309        assert!(result.is_err());
1310        let errors = result.unwrap_err();
1311
1312        assert!(errors.iter().any(|e| e.contains("Token EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v in allowed_spl_paid_tokens must also be in allowed_tokens")));
1313    }
1314
1315    // Helper to create a simple test that only validates programs (no tokens)
1316    fn create_program_only_config() -> Config {
1317        Config {
1318            validation: ValidationConfig {
1319                max_allowed_lamports: 1_000_000,
1320                max_signatures: 10,
1321                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1322                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()], // Required to pass basic validation
1323                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1324                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1325                ]),
1326                disallowed_accounts: vec![],
1327                price_source: PriceSource::Jupiter,
1328                fee_payer_policy: FeePayerPolicy::default(),
1329                price: PriceConfig { model: PriceModel::Free },
1330                token_2022: Token2022Config::default(),
1331                allow_durable_transactions: false,
1332                max_price_staleness_slots: 0,
1333            },
1334            metrics: MetricsConfig::default(),
1335            kora: KoraConfig::default(),
1336        }
1337    }
1338
1339    // Helper to create a simple test that only validates tokens (no programs)
1340    fn create_token_only_config() -> Config {
1341        Config {
1342            validation: ValidationConfig {
1343                max_allowed_lamports: 1_000_000,
1344                max_signatures: 10,
1345                allowed_programs: vec![], // No programs
1346                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1347                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), // Empty to avoid duplicate validation
1348                disallowed_accounts: vec![],
1349                price_source: PriceSource::Jupiter,
1350                fee_payer_policy: FeePayerPolicy::default(),
1351                price: PriceConfig { model: PriceModel::Free },
1352                token_2022: Token2022Config::default(),
1353                allow_durable_transactions: false,
1354                max_price_staleness_slots: 0,
1355            },
1356            metrics: MetricsConfig::default(),
1357            kora: KoraConfig::default(),
1358        }
1359    }
1360
1361    #[tokio::test]
1362    #[serial]
1363    async fn test_validate_with_result_rpc_validation_valid_program() {
1364        let config = create_program_only_config();
1365
1366        // Initialize global config
1367        let _ = update_config(config);
1368
1369        let rpc_client = create_mock_rpc_client_with_account(&create_mock_program_account());
1370
1371        // Test with RPC validation enabled (skip_rpc_validation = false)
1372        // The program validation should pass, but token validation will fail (AccountNotFound)
1373        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1374        assert!(result.is_err());
1375        let errors = result.unwrap_err();
1376        // Should have token validation errors (account not found), but no program validation errors
1377        assert!(errors.iter().any(|e| e.contains("Token")
1378            && e.contains("validation failed")
1379            && e.contains("not found")));
1380        assert!(!errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1381    }
1382
1383    #[tokio::test]
1384    #[serial]
1385    async fn test_validate_with_result_rpc_validation_valid_token_mint() {
1386        std::env::set_var("JUPITER_API_KEY", "test-api-key");
1387        let config = create_token_only_config();
1388
1389        // Initialize global config
1390        let _ = update_config(config);
1391
1392        let rpc_client = create_mock_rpc_client_with_mint(6);
1393
1394        // Test with RPC validation enabled (skip_rpc_validation = false)
1395        // Token validation should pass (mock returns token mint) since we have no programs
1396        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1397        assert!(result.is_ok());
1398        // Should have warnings about no programs but no errors
1399        let warnings = result.unwrap();
1400        assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
1401    }
1402
1403    #[tokio::test]
1404    #[serial]
1405    async fn test_validate_with_result_rpc_validation_non_executable_program_fails() {
1406        let config = Config {
1407            validation: ValidationConfig {
1408                max_allowed_lamports: 1_000_000,
1409                max_signatures: 10,
1410                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1411                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1412                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1413                disallowed_accounts: vec![],
1414                price_source: PriceSource::Jupiter,
1415                fee_payer_policy: FeePayerPolicy::default(),
1416                price: PriceConfig { model: PriceModel::Free },
1417                token_2022: Token2022Config::default(),
1418                allow_durable_transactions: false,
1419                max_price_staleness_slots: 0,
1420            },
1421            metrics: MetricsConfig::default(),
1422            kora: KoraConfig::default(),
1423        };
1424
1425        // Initialize global config
1426        let _ = update_config(config);
1427
1428        let rpc_client = create_mock_rpc_client_with_account(&create_mock_non_executable_account());
1429
1430        // Test with RPC validation enabled (skip_rpc_validation = false)
1431        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1432        assert!(result.is_err());
1433        let errors = result.unwrap_err();
1434        assert!(errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1435    }
1436
1437    #[tokio::test]
1438    #[serial]
1439    async fn test_validate_with_result_rpc_validation_account_not_found_fails() {
1440        let config = Config {
1441            validation: ValidationConfig {
1442                max_allowed_lamports: 1_000_000,
1443                max_signatures: 10,
1444                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1445                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1446                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1447                disallowed_accounts: vec![],
1448                price_source: PriceSource::Jupiter,
1449                fee_payer_policy: FeePayerPolicy::default(),
1450                price: PriceConfig { model: PriceModel::Free },
1451                token_2022: Token2022Config::default(),
1452                allow_durable_transactions: false,
1453                max_price_staleness_slots: 0,
1454            },
1455            metrics: MetricsConfig::default(),
1456            kora: KoraConfig::default(),
1457        };
1458
1459        let _ = update_config(config);
1460
1461        let rpc_client = create_mock_rpc_client_account_not_found();
1462
1463        // Test with RPC validation enabled (skip_rpc_validation = false)
1464        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1465        assert!(result.is_err());
1466        let errors = result.unwrap_err();
1467        assert!(errors.len() >= 2, "Should have validation errors for programs and tokens");
1468    }
1469
1470    #[tokio::test]
1471    #[serial]
1472    async fn test_validate_with_result_skip_rpc_validation() {
1473        std::env::set_var("JUPITER_API_KEY", "test-api-key");
1474        let config = Config {
1475            validation: ValidationConfig {
1476                max_allowed_lamports: 1_000_000,
1477                max_signatures: 10,
1478                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1479                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1480                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1481                disallowed_accounts: vec![],
1482                price_source: PriceSource::Jupiter,
1483                fee_payer_policy: FeePayerPolicy::default(),
1484                price: PriceConfig { model: PriceModel::Free },
1485                token_2022: Token2022Config::default(),
1486                allow_durable_transactions: false,
1487                max_price_staleness_slots: 0,
1488            },
1489            metrics: MetricsConfig::default(),
1490            kora: KoraConfig::default(),
1491        };
1492
1493        let _ = update_config(config);
1494
1495        // Use account not found RPC client - should not matter when skipping RPC validation
1496        let rpc_client = create_mock_rpc_client_account_not_found();
1497
1498        // Test with RPC validation disabled (skip_rpc_validation = true)
1499        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1500        assert!(result.is_ok()); // Should pass because RPC validation is skipped
1501    }
1502
1503    #[tokio::test]
1504    #[serial]
1505    async fn test_validate_with_result_valid_token2022_extensions() {
1506        std::env::set_var("JUPITER_API_KEY", "test-api-key");
1507        let config = Config {
1508            validation: ValidationConfig {
1509                max_allowed_lamports: 1_000_000,
1510                max_signatures: 10,
1511                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1512                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1513                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1514                disallowed_accounts: vec![],
1515                price_source: PriceSource::Jupiter,
1516                fee_payer_policy: FeePayerPolicy::default(),
1517                price: PriceConfig { model: PriceModel::Free },
1518                token_2022: {
1519                    let mut config = Token2022Config::default();
1520                    config.blocked_mint_extensions =
1521                        vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1522                    config.blocked_account_extensions =
1523                        vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1524                    config
1525                },
1526                allow_durable_transactions: false,
1527                max_price_staleness_slots: 0,
1528            },
1529            metrics: MetricsConfig::default(),
1530            kora: KoraConfig::default(),
1531        };
1532
1533        let _ = update_config(config);
1534
1535        let rpc_client = RpcClient::new_with_commitment(
1536            "http://localhost:8899".to_string(),
1537            CommitmentConfig::confirmed(),
1538        );
1539        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1540        assert!(result.is_ok());
1541    }
1542
1543    #[tokio::test]
1544    #[serial]
1545    async fn test_validate_with_result_invalid_token2022_mint_extension() {
1546        let config = Config {
1547            validation: ValidationConfig {
1548                max_allowed_lamports: 1_000_000,
1549                max_signatures: 10,
1550                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1551                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1552                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1553                disallowed_accounts: vec![],
1554                price_source: PriceSource::Jupiter,
1555                fee_payer_policy: FeePayerPolicy::default(),
1556                price: PriceConfig { model: PriceModel::Free },
1557                token_2022: {
1558                    let mut config = Token2022Config::default();
1559                    config.blocked_mint_extensions = vec!["invalid_mint_extension".to_string()];
1560                    config
1561                },
1562                allow_durable_transactions: false,
1563                max_price_staleness_slots: 0,
1564            },
1565            metrics: MetricsConfig::default(),
1566            kora: KoraConfig::default(),
1567        };
1568
1569        let _ = update_config(config);
1570
1571        let rpc_client = RpcClient::new_with_commitment(
1572            "http://localhost:8899".to_string(),
1573            CommitmentConfig::confirmed(),
1574        );
1575        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1576        assert!(result.is_err());
1577        let errors = result.unwrap_err();
1578        assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1579            && e.contains("Invalid mint extension name: 'invalid_mint_extension'")));
1580    }
1581
1582    #[tokio::test]
1583    #[serial]
1584    async fn test_validate_with_result_invalid_token2022_account_extension() {
1585        let config = Config {
1586            validation: ValidationConfig {
1587                max_allowed_lamports: 1_000_000,
1588                max_signatures: 10,
1589                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1590                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1591                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1592                disallowed_accounts: vec![],
1593                price_source: PriceSource::Jupiter,
1594                fee_payer_policy: FeePayerPolicy::default(),
1595                price: PriceConfig { model: PriceModel::Free },
1596                token_2022: {
1597                    let mut config = Token2022Config::default();
1598                    config.blocked_account_extensions =
1599                        vec!["invalid_account_extension".to_string()];
1600                    config
1601                },
1602                allow_durable_transactions: false,
1603                max_price_staleness_slots: 0,
1604            },
1605            metrics: MetricsConfig::default(),
1606            kora: KoraConfig::default(),
1607        };
1608
1609        let _ = update_config(config);
1610
1611        let rpc_client = RpcClient::new_with_commitment(
1612            "http://localhost:8899".to_string(),
1613            CommitmentConfig::confirmed(),
1614        );
1615        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1616        assert!(result.is_err());
1617        let errors = result.unwrap_err();
1618        assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1619            && e.contains("Invalid account extension name: 'invalid_account_extension'")));
1620    }
1621
1622    #[test]
1623    fn test_validate_token2022_extensions_valid() {
1624        let mut config = Token2022Config::default();
1625        config.blocked_mint_extensions =
1626            vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1627        config.blocked_account_extensions =
1628            vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1629
1630        let result = validate_token2022_extensions(&config);
1631        assert!(result.is_ok());
1632    }
1633
1634    #[test]
1635    fn test_validate_token2022_extensions_invalid_mint_extension() {
1636        let mut config = Token2022Config::default();
1637        config.blocked_mint_extensions = vec!["invalid_extension".to_string()];
1638
1639        let result = validate_token2022_extensions(&config);
1640        assert!(result.is_err());
1641        assert!(result.unwrap_err().contains("Invalid mint extension name: 'invalid_extension'"));
1642    }
1643
1644    #[test]
1645    fn test_validate_token2022_extensions_invalid_account_extension() {
1646        let mut config = Token2022Config::default();
1647        config.blocked_account_extensions = vec!["invalid_extension".to_string()];
1648
1649        let result = validate_token2022_extensions(&config);
1650        assert!(result.is_err());
1651        assert!(result
1652            .unwrap_err()
1653            .contains("Invalid account extension name: 'invalid_extension'"));
1654    }
1655
1656    #[test]
1657    fn test_validate_token2022_extensions_empty() {
1658        let config = Token2022Config::default();
1659
1660        let result = validate_token2022_extensions(&config);
1661        assert!(result.is_ok());
1662    }
1663
1664    #[tokio::test]
1665    #[serial]
1666    async fn test_validate_with_result_fee_payer_policy_warnings() {
1667        std::env::set_var("JUPITER_API_KEY", "test-api-key");
1668        let config = Config {
1669            validation: ValidationConfig {
1670                max_allowed_lamports: 1_000_000,
1671                max_signatures: 10,
1672                allowed_programs: vec![
1673                    SYSTEM_PROGRAM_ID.to_string(),
1674                    SPL_TOKEN_PROGRAM_ID.to_string(),
1675                    TOKEN_2022_PROGRAM_ID.to_string(),
1676                ],
1677                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1678                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1679                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1680                ]),
1681                disallowed_accounts: vec![],
1682                price_source: PriceSource::Jupiter,
1683                fee_payer_policy: FeePayerPolicy {
1684                    system: SystemInstructionPolicy {
1685                        allow_transfer: true,
1686                        allow_assign: true,
1687                        allow_create_account: true,
1688                        allow_allocate: true,
1689                        nonce: NonceInstructionPolicy {
1690                            allow_initialize: true,
1691                            allow_advance: true,
1692                            allow_withdraw: true,
1693                            allow_authorize: true,
1694                        },
1695                    },
1696                    spl_token: SplTokenInstructionPolicy {
1697                        allow_transfer: true,
1698                        allow_burn: true,
1699                        allow_close_account: true,
1700                        allow_approve: true,
1701                        allow_revoke: true,
1702                        allow_set_authority: true,
1703                        allow_mint_to: true,
1704                        allow_initialize_mint: true,
1705                        allow_initialize_account: true,
1706                        allow_initialize_multisig: true,
1707                        allow_freeze_account: true,
1708                        allow_thaw_account: true,
1709                    },
1710                    token_2022: Token2022InstructionPolicy {
1711                        allow_transfer: true,
1712                        allow_burn: true,
1713                        allow_close_account: true,
1714                        allow_approve: true,
1715                        allow_revoke: true,
1716                        allow_set_authority: true,
1717                        allow_mint_to: true,
1718                        allow_initialize_mint: true,
1719                        allow_initialize_account: true,
1720                        allow_initialize_multisig: true,
1721                        allow_freeze_account: true,
1722                        allow_thaw_account: true,
1723                    },
1724                },
1725                price: PriceConfig { model: PriceModel::Free },
1726                token_2022: Token2022Config::default(),
1727                allow_durable_transactions: false,
1728                max_price_staleness_slots: 0,
1729            },
1730            metrics: MetricsConfig::default(),
1731            kora: KoraConfig::default(),
1732        };
1733
1734        let _ = update_config(config.clone());
1735
1736        let rpc_client = RpcClient::new_with_commitment(
1737            "http://localhost:8899".to_string(),
1738            CommitmentConfig::confirmed(),
1739        );
1740        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1741        assert!(result.is_ok());
1742        let warnings = result.unwrap();
1743
1744        // Should have warnings for ALL enabled fee payer policy flags
1745        // System policies
1746        assert!(warnings
1747            .iter()
1748            .any(|w| w.contains("System transfers") && w.contains("allow_transfer")));
1749        assert!(warnings
1750            .iter()
1751            .any(|w| w.contains("System Assign instructions") && w.contains("allow_assign")));
1752        assert!(warnings.iter().any(|w| w.contains("System CreateAccount instructions")
1753            && w.contains("allow_create_account")));
1754        assert!(warnings
1755            .iter()
1756            .any(|w| w.contains("System Allocate instructions") && w.contains("allow_allocate")));
1757
1758        // Nonce policies
1759        assert!(warnings
1760            .iter()
1761            .any(|w| w.contains("nonce account initialization") && w.contains("allow_initialize")));
1762        assert!(warnings
1763            .iter()
1764            .any(|w| w.contains("nonce account advancement") && w.contains("allow_advance")));
1765        assert!(warnings
1766            .iter()
1767            .any(|w| w.contains("nonce account withdrawals") && w.contains("allow_withdraw")));
1768        assert!(warnings
1769            .iter()
1770            .any(|w| w.contains("nonce authority changes") && w.contains("allow_authorize")));
1771
1772        // SPL Token policies
1773        assert!(warnings
1774            .iter()
1775            .any(|w| w.contains("SPL Token transfers") && w.contains("allow_transfer")));
1776        assert!(warnings
1777            .iter()
1778            .any(|w| w.contains("SPL Token burn operations") && w.contains("allow_burn")));
1779        assert!(warnings
1780            .iter()
1781            .any(|w| w.contains("SPL Token CloseAccount") && w.contains("allow_close_account")));
1782        assert!(warnings
1783            .iter()
1784            .any(|w| w.contains("SPL Token approve") && w.contains("allow_approve")));
1785        assert!(warnings
1786            .iter()
1787            .any(|w| w.contains("SPL Token revoke") && w.contains("allow_revoke")));
1788        assert!(warnings
1789            .iter()
1790            .any(|w| w.contains("SPL Token SetAuthority") && w.contains("allow_set_authority")));
1791        assert!(warnings
1792            .iter()
1793            .any(|w| w.contains("SPL Token MintTo") && w.contains("allow_mint_to")));
1794        assert!(
1795            warnings
1796                .iter()
1797                .any(|w| w.contains("SPL Token InitializeMint")
1798                    && w.contains("allow_initialize_mint"))
1799        );
1800        assert!(warnings
1801            .iter()
1802            .any(|w| w.contains("SPL Token InitializeAccount")
1803                && w.contains("allow_initialize_account")));
1804        assert!(warnings.iter().any(|w| w.contains("SPL Token InitializeMultisig")
1805            && w.contains("allow_initialize_multisig")));
1806        assert!(warnings
1807            .iter()
1808            .any(|w| w.contains("SPL Token FreezeAccount") && w.contains("allow_freeze_account")));
1809        assert!(warnings
1810            .iter()
1811            .any(|w| w.contains("SPL Token ThawAccount") && w.contains("allow_thaw_account")));
1812
1813        // Token2022 policies
1814        assert!(warnings
1815            .iter()
1816            .any(|w| w.contains("Token2022 transfers") && w.contains("allow_transfer")));
1817        assert!(warnings
1818            .iter()
1819            .any(|w| w.contains("Token2022 burn operations") && w.contains("allow_burn")));
1820        assert!(warnings
1821            .iter()
1822            .any(|w| w.contains("Token2022 CloseAccount") && w.contains("allow_close_account")));
1823        assert!(warnings
1824            .iter()
1825            .any(|w| w.contains("Token2022 approve") && w.contains("allow_approve")));
1826        assert!(warnings
1827            .iter()
1828            .any(|w| w.contains("Token2022 revoke") && w.contains("allow_revoke")));
1829        assert!(warnings
1830            .iter()
1831            .any(|w| w.contains("Token2022 SetAuthority") && w.contains("allow_set_authority")));
1832        assert!(warnings
1833            .iter()
1834            .any(|w| w.contains("Token2022 MintTo") && w.contains("allow_mint_to")));
1835        assert!(
1836            warnings
1837                .iter()
1838                .any(|w| w.contains("Token2022 InitializeMint")
1839                    && w.contains("allow_initialize_mint"))
1840        );
1841        assert!(warnings
1842            .iter()
1843            .any(|w| w.contains("Token2022 InitializeAccount")
1844                && w.contains("allow_initialize_account")));
1845        assert!(warnings.iter().any(|w| w.contains("Token2022 InitializeMultisig")
1846            && w.contains("allow_initialize_multisig")));
1847        assert!(warnings
1848            .iter()
1849            .any(|w| w.contains("Token2022 FreezeAccount") && w.contains("allow_freeze_account")));
1850        assert!(warnings
1851            .iter()
1852            .any(|w| w.contains("Token2022 ThawAccount") && w.contains("allow_thaw_account")));
1853
1854        // Each warning should contain risk explanation
1855        let fee_payer_warnings: Vec<_> =
1856            warnings.iter().filter(|w| w.contains("Fee payer policy")).collect();
1857        for warning in fee_payer_warnings {
1858            assert!(warning.contains("Risk:"));
1859            assert!(warning.contains("Consider setting"));
1860        }
1861    }
1862
1863    #[tokio::test]
1864    #[serial]
1865    async fn test_check_token_mint_extensions_permanent_delegate() {
1866        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1867
1868        let mint_with_delegate =
1869            create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::PermanentDelegate]);
1870        let mint_pubkey = Pubkey::new_unique();
1871
1872        let rpc_client = create_mock_rpc_client_with_account(&mint_with_delegate);
1873        let mut warnings = Vec::new();
1874
1875        ConfigValidator::check_token_mint_extensions(
1876            &rpc_client,
1877            &[mint_pubkey.to_string()],
1878            &mut warnings,
1879        )
1880        .await;
1881
1882        assert_eq!(warnings.len(), 1);
1883        assert!(warnings[0].contains("PermanentDelegate extension"));
1884        assert!(warnings[0].contains(&mint_pubkey.to_string()));
1885        assert!(warnings[0].contains("Risk:"));
1886        assert!(warnings[0].contains("permanent delegate can transfer or burn tokens"));
1887    }
1888
1889    #[tokio::test]
1890    #[serial]
1891    async fn test_check_token_mint_extensions_transfer_hook() {
1892        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1893
1894        let mint_with_hook =
1895            create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::TransferHook]);
1896        let mint_pubkey = Pubkey::new_unique();
1897
1898        let rpc_client = create_mock_rpc_client_with_account(&mint_with_hook);
1899        let mut warnings = Vec::new();
1900
1901        ConfigValidator::check_token_mint_extensions(
1902            &rpc_client,
1903            &[mint_pubkey.to_string()],
1904            &mut warnings,
1905        )
1906        .await;
1907
1908        assert_eq!(warnings.len(), 1);
1909        assert!(warnings[0].contains("TransferHook extension"));
1910        assert!(warnings[0].contains(&mint_pubkey.to_string()));
1911        assert!(warnings[0].contains("Risk:"));
1912        assert!(warnings[0].contains("custom program executes on every transfer"));
1913    }
1914
1915    #[tokio::test]
1916    #[serial]
1917    async fn test_check_token_mint_extensions_both() {
1918        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1919
1920        let mint_with_both = create_mock_token2022_mint_with_extensions(
1921            6,
1922            vec![ExtensionType::PermanentDelegate, ExtensionType::TransferHook],
1923        );
1924        let mint_pubkey = Pubkey::new_unique();
1925
1926        let rpc_client = create_mock_rpc_client_with_account(&mint_with_both);
1927        let mut warnings = Vec::new();
1928
1929        ConfigValidator::check_token_mint_extensions(
1930            &rpc_client,
1931            &[mint_pubkey.to_string()],
1932            &mut warnings,
1933        )
1934        .await;
1935
1936        // Should have warnings for both extensions
1937        assert_eq!(warnings.len(), 2);
1938        assert!(warnings.iter().any(|w| w.contains("PermanentDelegate extension")));
1939        assert!(warnings.iter().any(|w| w.contains("TransferHook extension")));
1940    }
1941
1942    #[tokio::test]
1943    #[serial]
1944    async fn test_check_token_mint_extensions_no_risky_extensions() {
1945        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1946
1947        let mint_with_safe =
1948            create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::MintCloseAuthority]);
1949        let mint_pubkey = Pubkey::new_unique();
1950
1951        let rpc_client = create_mock_rpc_client_with_account(&mint_with_safe);
1952        let mut warnings = Vec::new();
1953
1954        ConfigValidator::check_token_mint_extensions(
1955            &rpc_client,
1956            &[mint_pubkey.to_string()],
1957            &mut warnings,
1958        )
1959        .await;
1960
1961        assert_eq!(warnings.len(), 0);
1962    }
1963
1964    #[tokio::test]
1965    #[serial]
1966    async fn test_durable_transactions_warning_when_enabled() {
1967        std::env::set_var("JUPITER_API_KEY", "test-api-key");
1968        let config = Config {
1969            validation: ValidationConfig {
1970                max_allowed_lamports: 1_000_000,
1971                max_signatures: 10,
1972                allowed_programs: vec![
1973                    SYSTEM_PROGRAM_ID.to_string(),
1974                    SPL_TOKEN_PROGRAM_ID.to_string(),
1975                ],
1976                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1977                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1978                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1979                ]),
1980                disallowed_accounts: vec![],
1981                price_source: PriceSource::Jupiter,
1982                fee_payer_policy: FeePayerPolicy::default(),
1983                price: PriceConfig::default(),
1984                token_2022: Token2022Config::default(),
1985                allow_durable_transactions: true, // Enabled - should warn
1986                max_price_staleness_slots: 0,
1987            },
1988            kora: KoraConfig::default(),
1989            metrics: MetricsConfig::default(),
1990        };
1991
1992        let _ = update_config(config);
1993
1994        let rpc_client = RpcMockBuilder::new().build();
1995        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1996        assert!(result.is_ok());
1997        let warnings = result.unwrap();
1998
1999        assert!(warnings.iter().any(|w| w.contains("allow_durable_transactions is enabled")));
2000        assert!(warnings.iter().any(|w| w.contains("hold signed transactions indefinitely")));
2001    }
2002
2003    #[tokio::test]
2004    #[serial]
2005    async fn test_durable_transactions_no_warning_when_disabled() {
2006        std::env::set_var("JUPITER_API_KEY", "test-api-key");
2007        let config = Config {
2008            validation: ValidationConfig {
2009                max_allowed_lamports: 1_000_000,
2010                max_signatures: 10,
2011                allowed_programs: vec![
2012                    SYSTEM_PROGRAM_ID.to_string(),
2013                    SPL_TOKEN_PROGRAM_ID.to_string(),
2014                ],
2015                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2016                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2017                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2018                ]),
2019                disallowed_accounts: vec![],
2020                price_source: PriceSource::Jupiter,
2021                fee_payer_policy: FeePayerPolicy::default(),
2022                price: PriceConfig::default(),
2023                token_2022: Token2022Config::default(),
2024                allow_durable_transactions: false, // Disabled - should not warn
2025                max_price_staleness_slots: 0,
2026            },
2027            kora: KoraConfig::default(),
2028            metrics: MetricsConfig::default(),
2029        };
2030
2031        let _ = update_config(config);
2032
2033        let rpc_client = RpcMockBuilder::new().build();
2034        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2035        assert!(result.is_ok());
2036        let warnings = result.unwrap();
2037
2038        assert!(!warnings.iter().any(|w| w.contains("allow_durable_transactions")));
2039    }
2040
2041    #[tokio::test]
2042    #[serial]
2043    async fn test_jupiter_price_source_requires_api_key() {
2044        // Ensure JUPITER_API_KEY is not set
2045        std::env::remove_var("JUPITER_API_KEY");
2046
2047        let config = Config {
2048            validation: ValidationConfig {
2049                max_allowed_lamports: 1_000_000,
2050                max_signatures: 10,
2051                allowed_programs: vec![
2052                    SYSTEM_PROGRAM_ID.to_string(),
2053                    SPL_TOKEN_PROGRAM_ID.to_string(),
2054                ],
2055                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2056                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2057                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2058                ]),
2059                disallowed_accounts: vec![],
2060                price_source: PriceSource::Jupiter, // Jupiter requires API key
2061                fee_payer_policy: FeePayerPolicy::default(),
2062                price: PriceConfig::default(),
2063                token_2022: Token2022Config::default(),
2064                allow_durable_transactions: false,
2065                max_price_staleness_slots: 0,
2066            },
2067            kora: KoraConfig::default(),
2068            metrics: MetricsConfig::default(),
2069        };
2070
2071        let _ = update_config(config);
2072
2073        let rpc_client = RpcMockBuilder::new().build();
2074        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2075
2076        // Should fail because JUPITER_API_KEY is not set
2077        assert!(result.is_err());
2078        let errors = result.unwrap_err();
2079        assert!(errors.iter().any(|e| e.contains("JUPITER_API_KEY")));
2080        assert!(errors.iter().any(|e| e.contains("price_source = Jupiter")));
2081    }
2082
2083    #[tokio::test]
2084    #[serial]
2085    async fn test_lighthouse_enabled_not_in_allowed_programs_error() {
2086        let config = Config {
2087            validation: ValidationConfig {
2088                max_allowed_lamports: 1_000_000,
2089                max_signatures: 10,
2090                allowed_programs: vec![
2091                    SYSTEM_PROGRAM_ID.to_string(),
2092                    SPL_TOKEN_PROGRAM_ID.to_string(),
2093                ], // Lighthouse program NOT included
2094                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2095                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2096                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2097                ]),
2098                disallowed_accounts: vec![],
2099                price_source: PriceSource::Jupiter,
2100                fee_payer_policy: FeePayerPolicy::default(),
2101                price: PriceConfig::default(),
2102                token_2022: Token2022Config::default(),
2103                allow_durable_transactions: false,
2104                max_price_staleness_slots: 0,
2105            },
2106            kora: KoraConfig {
2107                lighthouse: LighthouseConfig {
2108                    enabled: true,
2109                    fail_if_transaction_size_overflow: true,
2110                },
2111                ..Default::default()
2112            },
2113            metrics: MetricsConfig::default(),
2114        };
2115
2116        let _ = update_config(config);
2117
2118        let rpc_client = RpcMockBuilder::new().build();
2119        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2120        assert!(result.is_err());
2121        let errors = result.unwrap_err();
2122
2123        assert!(errors
2124            .iter()
2125            .any(|e| e.contains("Lighthouse is enabled but")
2126                && e.contains("is not in allowed_programs")));
2127    }
2128
2129    #[tokio::test]
2130    #[serial]
2131    async fn test_lighthouse_enabled_in_disallowed_accounts_error() {
2132        let config = Config {
2133            validation: ValidationConfig {
2134                max_allowed_lamports: 1_000_000,
2135                max_signatures: 10,
2136                allowed_programs: vec![
2137                    SYSTEM_PROGRAM_ID.to_string(),
2138                    SPL_TOKEN_PROGRAM_ID.to_string(),
2139                    LIGHTHOUSE_PROGRAM_ID.to_string(), // Lighthouse in allowed
2140                ],
2141                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2142                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2143                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2144                ]),
2145                disallowed_accounts: vec![LIGHTHOUSE_PROGRAM_ID.to_string()], // Also in disallowed!
2146                price_source: PriceSource::Jupiter,
2147                fee_payer_policy: FeePayerPolicy::default(),
2148                price: PriceConfig::default(),
2149                token_2022: Token2022Config::default(),
2150                allow_durable_transactions: false,
2151                max_price_staleness_slots: 0,
2152            },
2153            kora: KoraConfig {
2154                lighthouse: LighthouseConfig {
2155                    enabled: true,
2156                    fail_if_transaction_size_overflow: true,
2157                },
2158                ..Default::default()
2159            },
2160            metrics: MetricsConfig::default(),
2161        };
2162
2163        let _ = update_config(config);
2164
2165        let rpc_client = RpcMockBuilder::new().build();
2166        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2167        assert!(result.is_err());
2168        let errors = result.unwrap_err();
2169
2170        assert!(errors
2171            .iter()
2172            .any(|e| e.contains("Lighthouse is enabled but")
2173                && e.contains("is in disallowed_accounts")));
2174    }
2175
2176    #[tokio::test]
2177    #[serial]
2178    async fn test_lighthouse_disabled_no_validation() {
2179        std::env::set_var("JUPITER_API_KEY", "test-api-key");
2180        let config = Config {
2181            validation: ValidationConfig {
2182                max_allowed_lamports: 1_000_000,
2183                max_signatures: 10,
2184                allowed_programs: vec![
2185                    SYSTEM_PROGRAM_ID.to_string(),
2186                    SPL_TOKEN_PROGRAM_ID.to_string(),
2187                ], // Lighthouse NOT included - but should be OK since disabled
2188                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2189                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2190                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2191                ]),
2192                disallowed_accounts: vec![],
2193                price_source: PriceSource::Jupiter,
2194                fee_payer_policy: FeePayerPolicy::default(),
2195                price: PriceConfig::default(),
2196                token_2022: Token2022Config::default(),
2197                allow_durable_transactions: false,
2198                max_price_staleness_slots: 0,
2199            },
2200            kora: KoraConfig {
2201                lighthouse: LighthouseConfig {
2202                    enabled: false, // Disabled
2203                    fail_if_transaction_size_overflow: true,
2204                },
2205                ..Default::default()
2206            },
2207            metrics: MetricsConfig::default(),
2208        };
2209
2210        let _ = update_config(config);
2211
2212        let rpc_client = RpcMockBuilder::new().build();
2213        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2214        assert!(result.is_ok());
2215        let warnings = result.unwrap();
2216
2217        // Should not have lighthouse errors since it's disabled
2218        assert!(!warnings.iter().any(|w| w.contains("Lighthouse")));
2219    }
2220}