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