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        if config.validation.allow_durable_transactions {
309            warnings.push(
310                "⚠️  SECURITY: allow_durable_transactions is enabled. \
311                Risk: Users can hold signed transactions indefinitely and execute them much later. \
312                Token values may change, your fee payer may run low on funds, or you may no longer \
313                want to subsidize these transactions. \
314                Consider disabling durable transactions unless specifically required."
315                    .to_string(),
316            );
317        }
318
319        // Validate allowed programs (warn if empty or missing system/token programs)
320        if config.validation.allowed_programs.is_empty() {
321            warnings.push(
322                "No allowed programs configured - this will block all transactions".to_string(),
323            );
324        } else {
325            if !config.validation.allowed_programs.contains(&SYSTEM_PROGRAM_ID.to_string()) {
326                warnings.push("Missing System Program in allowed programs - SOL transfers and account operations will be blocked".to_string());
327            }
328            if !config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string())
329                && !config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string())
330            {
331                warnings.push("Missing Token Program in allowed programs - SPL token operations will be blocked".to_string());
332            }
333        }
334
335        // Validate allowed tokens
336        if config.validation.allowed_tokens.is_empty() {
337            errors.push("No allowed tokens configured".to_string());
338        } else if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.allowed_tokens) {
339            errors.push(format!("Invalid token address: {e}"));
340        }
341
342        // Validate allowed spl paid tokens
343        if let Err(e) =
344            TokenUtil::check_valid_tokens(config.validation.allowed_spl_paid_tokens.as_slice())
345        {
346            errors.push(format!("Invalid spl paid token address: {e}"));
347        }
348
349        // Warn if using "All" for allowed_spl_paid_tokens
350        if matches!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All) {
351            warnings.push(
352                "⚠️  Using 'All' for allowed_spl_paid_tokens - this accepts ANY SPL token for payment. \
353                Consider using an explicit allowlist to reduce volatility risk and protect against \
354                potentially malicious or worthless tokens being used for fees.".to_string()
355            );
356        }
357
358        // Validate disallowed accounts
359        if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
360            errors.push(format!("Invalid disallowed account address: {e}"));
361        }
362
363        // Validate Token2022 extensions
364        if let Err(e) = validate_token2022_extensions(&config.validation.token_2022) {
365            errors.push(format!("Token2022 extension validation failed: {e}"));
366        }
367
368        // Warn if PermanentDelegate is not blocked
369        if !config.validation.token_2022.is_mint_extension_blocked(ExtensionType::PermanentDelegate)
370        {
371            warnings.push(
372                "⚠️  SECURITY: PermanentDelegate extension is NOT blocked. Tokens with this extension \
373                allow the delegate to transfer/burn tokens at any time without owner approval. \
374                This creates significant risks:\n\
375                  - Payment tokens: Funds can be seized after payment\n\
376                Consider adding \"permanent_delegate\" to blocked_mint_extensions in [validation.token2022] \
377                unless explicitly needed for your use case.".to_string()
378            );
379        }
380
381        // Check if fees are enabled (not Free pricing)
382        let fees_enabled = !matches!(config.validation.price.model, PriceModel::Free);
383
384        if fees_enabled {
385            // If fees enabled, token or token22 must be enabled in allowed_programs
386            let has_token_program =
387                config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string());
388            let has_token22_program =
389                config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string());
390
391            if !has_token_program && !has_token22_program {
392                errors.push("When fees are enabled, at least one token program (SPL Token or Token2022) must be in allowed_programs".to_string());
393            }
394
395            // If fees enabled, allowed_spl_paid_tokens can't be empty
396            if !config.validation.allowed_spl_paid_tokens.has_tokens() {
397                errors.push(
398                    "When fees are enabled, allowed_spl_paid_tokens cannot be empty".to_string(),
399                );
400            }
401        } else {
402            warnings.push(
403                "⚠️  SECURITY: Free pricing model enabled - all transactions will be processed \
404                without charging fees."
405                    .to_string(),
406            );
407        }
408
409        // Validate that all tokens in allowed_spl_paid_tokens are also in allowed_tokens
410        for paid_token in &config.validation.allowed_spl_paid_tokens {
411            if !config.validation.allowed_tokens.contains(paid_token) {
412                errors.push(format!(
413                    "Token {paid_token} in allowed_spl_paid_tokens must also be in allowed_tokens"
414                ));
415            }
416        }
417
418        // Validate fee payer policy - warn about enabled risky operations
419        Self::validate_fee_payer_policy(&config.validation.fee_payer_policy, &mut warnings);
420
421        // Validate margin (error if negative)
422        match &config.validation.price.model {
423            PriceModel::Fixed { amount, token, strict } => {
424                if *amount == 0 {
425                    warnings
426                        .push("Fixed price amount is 0 - transactions will be free".to_string());
427                }
428                if Pubkey::from_str(token).is_err() {
429                    errors.push(format!("Invalid token address for fixed price: {token}"));
430                }
431                if !config.validation.supports_token(token) {
432                    errors.push(format!(
433                        "Token address for fixed price is not in allowed spl paid tokens: {token}"
434                    ));
435                }
436
437                // Warn about dangerous configurations with fixed pricing
438                let has_auth =
439                    config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
440                if !has_auth {
441                    warnings.push(
442                        "⚠️  SECURITY: Fixed pricing with NO authentication enabled. \
443                        Without authentication, anyone can spam transactions at your expense. \
444                        Consider enabling api_key or hmac_secret in [kora.auth]."
445                            .to_string(),
446                    );
447                }
448
449                // Warn about strict mode
450                if *strict {
451                    warnings.push(
452                        "Strict pricing mode enabled. \
453                        Transactions where fee payer outflow exceeds the fixed price will be rejected."
454                            .to_string(),
455                    );
456                }
457            }
458            PriceModel::Margin { margin } => {
459                if *margin < 0.0 {
460                    errors.push("Margin cannot be negative".to_string());
461                } else if *margin > 1.0 {
462                    warnings.push(format!("Margin is {}% - this is very high", margin * 100.0));
463                }
464            }
465            _ => {}
466        };
467
468        // General authentication warning
469        let has_auth = config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
470        if !has_auth {
471            warnings.push(
472                "⚠️  SECURITY: No authentication configured (neither api_key nor hmac_secret). \
473                Authentication is strongly recommended for production deployments. \
474                Consider enabling api_key or hmac_secret in [kora.auth]."
475                    .to_string(),
476            );
477        }
478
479        // Validate usage limit configuration
480        let usage_config = &config.kora.usage_limit;
481        if usage_config.enabled {
482            let (usage_errors, usage_warnings) = CacheValidator::validate(usage_config).await;
483            errors.extend(usage_errors);
484            warnings.extend(usage_warnings);
485        }
486
487        // RPC validation - only if not skipped
488        if !skip_rpc_validation {
489            // Validate allowed programs - should be executable
490            for program_str in &config.validation.allowed_programs {
491                if let Ok(program_pubkey) = Pubkey::from_str(program_str) {
492                    if let Err(e) = validate_account(
493                        config,
494                        rpc_client,
495                        &program_pubkey,
496                        Some(AccountType::Program),
497                    )
498                    .await
499                    {
500                        errors.push(format!("Program {program_str} validation failed: {e}"));
501                    }
502                }
503            }
504
505            // Validate allowed tokens - should be non-executable token mints
506            for token_str in &config.validation.allowed_tokens {
507                if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
508                    if let Err(e) =
509                        validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
510                            .await
511                    {
512                        errors.push(format!("Token {token_str} validation failed: {e}"));
513                    }
514                }
515            }
516
517            // Validate allowed spl paid tokens - should be non-executable token mints
518            for token_str in &config.validation.allowed_spl_paid_tokens {
519                if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
520                    if let Err(e) =
521                        validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
522                            .await
523                    {
524                        errors.push(format!("SPL paid token {token_str} validation failed: {e}"));
525                    }
526                }
527            }
528
529            // Check Token2022 mints for risky extensions
530            Self::check_token_mint_extensions(
531                rpc_client,
532                &config.validation.allowed_tokens,
533                &mut warnings,
534            )
535            .await;
536
537            // Validate missing ATAs for payment address
538            if let Some(payment_address) = &config.kora.payment_address {
539                if let Ok(payment_address) = Pubkey::from_str(payment_address) {
540                    match find_missing_atas(config, rpc_client, &payment_address).await {
541                        Ok(atas_to_create) => {
542                            if !atas_to_create.is_empty() {
543                                errors.push(format!(
544                                    "Missing ATAs for payment address: {payment_address}"
545                                ));
546                            }
547                        }
548                        Err(e) => errors.push(format!("Failed to find missing ATAs: {e}")),
549                    }
550                } else {
551                    errors.push(format!("Invalid payment address: {payment_address}"));
552                }
553            }
554        }
555
556        // Validate signers configuration if provided
557        if let Some(path) = signers_config_path {
558            match SignerPoolConfig::load_config(path.as_ref()) {
559                Ok(signer_config) => {
560                    let (signer_warnings, signer_errors) =
561                        SignerValidator::validate_with_result(&signer_config);
562                    warnings.extend(signer_warnings);
563                    errors.extend(signer_errors);
564                }
565                Err(e) => {
566                    errors.push(format!("Failed to load signers config: {e}"));
567                }
568            }
569        } else {
570            println!("ℹ️  Signers configuration not validated. Include --signers-config path/to/signers.toml to validate signers");
571        }
572
573        // Output results
574        println!("=== Configuration Validation ===");
575        if errors.is_empty() {
576            println!("✓ Configuration validation successful!");
577        } else {
578            println!("✗ Configuration validation failed!");
579            println!("\n❌ Errors:");
580            for error in &errors {
581                println!("   - {error}");
582            }
583            println!("\nPlease fix the configuration errors above before deploying.");
584        }
585
586        if !warnings.is_empty() {
587            println!("\n⚠️  Warnings:");
588            for warning in &warnings {
589                println!("   - {warning}");
590            }
591        }
592
593        if errors.is_empty() {
594            Ok(warnings)
595        } else {
596            Err(errors)
597        }
598    }
599}
600
601/// Validate Token2022 extension configuration
602fn validate_token2022_extensions(config: &Token2022Config) -> Result<(), String> {
603    // Validate blocked mint extensions
604    for ext_name in &config.blocked_mint_extensions {
605        if spl_token_2022_util::parse_mint_extension_string(ext_name).is_none() {
606            return Err(format!(
607                "Invalid mint extension name: '{ext_name}'. Valid names are: {:?}",
608                spl_token_2022_util::get_all_mint_extension_names()
609            ));
610        }
611    }
612
613    // Validate blocked account extensions
614    for ext_name in &config.blocked_account_extensions {
615        if spl_token_2022_util::parse_account_extension_string(ext_name).is_none() {
616            return Err(format!(
617                "Invalid account extension name: '{ext_name}'. Valid names are: {:?}",
618                spl_token_2022_util::get_all_account_extension_names()
619            ));
620        }
621    }
622
623    Ok(())
624}
625
626#[cfg(test)]
627mod tests {
628    use crate::{
629        config::{
630            AuthConfig, BundleConfig, CacheConfig, Config, EnabledMethods, FeePayerPolicy,
631            KoraConfig, MetricsConfig, NonceInstructionPolicy, SplTokenConfig,
632            SplTokenInstructionPolicy, SystemInstructionPolicy, Token2022InstructionPolicy,
633            UsageLimitConfig, ValidationConfig,
634        },
635        constant::DEFAULT_MAX_REQUEST_BODY_SIZE,
636        fee::price::PriceConfig,
637        state::update_config,
638        tests::{
639            account_mock::create_mock_token2022_mint_with_extensions,
640            common::{
641                create_mock_non_executable_account, create_mock_program_account,
642                create_mock_rpc_client_account_not_found, create_mock_rpc_client_with_account,
643                create_mock_rpc_client_with_mint, RpcMockBuilder,
644            },
645            config_mock::ConfigMockBuilder,
646        },
647    };
648    use serial_test::serial;
649    use solana_commitment_config::CommitmentConfig;
650    use spl_token_2022_interface::extension::ExtensionType;
651
652    use super::*;
653
654    #[tokio::test]
655    #[serial]
656    async fn test_validate_config() {
657        let mut config = Config {
658            validation: ValidationConfig {
659                max_allowed_lamports: 1000000000,
660                max_signatures: 10,
661                allowed_programs: vec!["program1".to_string()],
662                allowed_tokens: vec!["token1".to_string()],
663                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec!["token3".to_string()]),
664                disallowed_accounts: vec!["account1".to_string()],
665                price_source: PriceSource::Jupiter,
666                fee_payer_policy: FeePayerPolicy::default(),
667                price: PriceConfig::default(),
668                token_2022: Token2022Config::default(),
669                allow_durable_transactions: false,
670            },
671            kora: KoraConfig::default(),
672            metrics: MetricsConfig::default(),
673        };
674
675        // Initialize global config
676        let _ = update_config(config.clone());
677
678        // Test empty tokens list
679        config.validation.allowed_tokens = vec![];
680        let _ = update_config(config);
681
682        let rpc_client = RpcClient::new_with_commitment(
683            "http://localhost:8899".to_string(),
684            CommitmentConfig::confirmed(),
685        );
686        let result = ConfigValidator::validate(&rpc_client).await;
687        assert!(result.is_err());
688        assert!(matches!(result.unwrap_err(), KoraError::InternalServerError(_)));
689    }
690
691    #[tokio::test]
692    #[serial]
693    async fn test_validate_with_result_successful_config() {
694        let config = Config {
695            validation: ValidationConfig {
696                max_allowed_lamports: 1_000_000,
697                max_signatures: 10,
698                allowed_programs: vec![
699                    SYSTEM_PROGRAM_ID.to_string(),
700                    SPL_TOKEN_PROGRAM_ID.to_string(),
701                ],
702                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
703                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
704                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
705                ]),
706                disallowed_accounts: vec![],
707                price_source: PriceSource::Jupiter,
708                fee_payer_policy: FeePayerPolicy::default(),
709                price: PriceConfig::default(),
710                token_2022: Token2022Config::default(),
711                allow_durable_transactions: false,
712            },
713            kora: KoraConfig::default(),
714            metrics: MetricsConfig::default(),
715        };
716
717        // Initialize global config
718        let _ = update_config(config);
719
720        let rpc_client = RpcClient::new_with_commitment(
721            "http://localhost:8899".to_string(),
722            CommitmentConfig::confirmed(),
723        );
724        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
725        assert!(result.is_ok());
726        let warnings = result.unwrap();
727        // Expect warnings about PermanentDelegate and no authentication
728        assert_eq!(warnings.len(), 2);
729        assert!(warnings.iter().any(|w| w.contains("PermanentDelegate")));
730        assert!(warnings.iter().any(|w| w.contains("No authentication configured")));
731    }
732
733    #[tokio::test]
734    #[serial]
735    async fn test_validate_with_result_warnings() {
736        let config = Config {
737            validation: ValidationConfig {
738                max_allowed_lamports: 0,  // Should warn
739                max_signatures: 0,        // Should warn
740                allowed_programs: vec![], // Should warn
741                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
742                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
743                disallowed_accounts: vec![],
744                price_source: PriceSource::Mock, // Should warn
745                fee_payer_policy: FeePayerPolicy::default(),
746                price: PriceConfig { model: PriceModel::Free },
747                token_2022: Token2022Config::default(),
748                allow_durable_transactions: false,
749            },
750            kora: KoraConfig {
751                rate_limit: 0, // Should warn
752                max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
753                enabled_methods: EnabledMethods {
754                    liveness: false,
755                    estimate_transaction_fee: false,
756                    get_supported_tokens: false,
757                    sign_transaction: false,
758                    sign_and_send_transaction: false,
759                    transfer_transaction: false,
760                    get_blockhash: false,
761                    get_config: false,
762                    get_payer_signer: false,
763                    get_version: false,
764                    estimate_bundle_fee: false,
765                    sign_and_send_bundle: false,
766                    sign_bundle: false,
767                },
768                auth: AuthConfig::default(),
769                payment_address: None,
770                cache: CacheConfig::default(),
771                usage_limit: UsageLimitConfig::default(),
772                bundle: BundleConfig::default(),
773            },
774            metrics: MetricsConfig::default(),
775        };
776
777        // Initialize global config
778        let _ = update_config(config);
779
780        let rpc_client = RpcClient::new_with_commitment(
781            "http://localhost:8899".to_string(),
782            CommitmentConfig::confirmed(),
783        );
784        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
785        assert!(result.is_ok());
786        let warnings = result.unwrap();
787
788        assert!(!warnings.is_empty());
789        assert!(warnings.iter().any(|w| w.contains("Rate limit is set to 0")));
790        assert!(warnings.iter().any(|w| w.contains("All rpc methods are disabled")));
791        assert!(warnings.iter().any(|w| w.contains("Max allowed lamports is 0")));
792        assert!(warnings.iter().any(|w| w.contains("Max signatures is 0")));
793        assert!(warnings.iter().any(|w| w.contains("Using Mock price source")));
794        assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
795    }
796
797    #[tokio::test]
798    #[serial]
799    async fn test_validate_with_result_missing_system_program_warning() {
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        let config = Config {
967            validation: ValidationConfig {
968                max_allowed_lamports: 1_000_000,
969                max_signatures: 10,
970                allowed_programs: vec![
971                    SYSTEM_PROGRAM_ID.to_string(),
972                    SPL_TOKEN_PROGRAM_ID.to_string(),
973                ],
974                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
975                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
976                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
977                ]),
978                disallowed_accounts: vec![],
979                price_source: PriceSource::Jupiter,
980                fee_payer_policy: FeePayerPolicy::default(),
981                price: PriceConfig {
982                    model: PriceModel::Fixed {
983                        amount: 0, // Should warn
984                        token: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
985                        strict: false,
986                    },
987                },
988                token_2022: Token2022Config::default(),
989                allow_durable_transactions: false,
990            },
991            metrics: MetricsConfig::default(),
992            kora: KoraConfig::default(),
993        };
994
995        let _ = update_config(config);
996
997        let rpc_client = RpcClient::new_with_commitment(
998            "http://localhost:8899".to_string(),
999            CommitmentConfig::confirmed(),
1000        );
1001        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1002        assert!(result.is_ok());
1003        let warnings = result.unwrap();
1004
1005        assert!(warnings
1006            .iter()
1007            .any(|w| w.contains("Fixed price amount is 0 - transactions will be free")));
1008    }
1009
1010    #[tokio::test]
1011    #[serial]
1012    async fn test_validate_with_result_fee_validation_errors() {
1013        let config = Config {
1014            validation: ValidationConfig {
1015                max_allowed_lamports: 1_000_000,
1016                max_signatures: 10,
1017                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()], // Missing token programs
1018                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1019                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), // Empty when fees enabled - should error
1020                disallowed_accounts: vec![],
1021                price_source: PriceSource::Jupiter,
1022                fee_payer_policy: FeePayerPolicy::default(),
1023                price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1024                token_2022: Token2022Config::default(),
1025                allow_durable_transactions: false,
1026            },
1027            metrics: MetricsConfig::default(),
1028            kora: KoraConfig::default(),
1029        };
1030
1031        let _ = update_config(config);
1032
1033        let rpc_client = RpcClient::new_with_commitment(
1034            "http://localhost:8899".to_string(),
1035            CommitmentConfig::confirmed(),
1036        );
1037        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1038        assert!(result.is_err());
1039        let errors = result.unwrap_err();
1040
1041        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")));
1042        assert!(errors
1043            .iter()
1044            .any(|e| e.contains("When fees are enabled, allowed_spl_paid_tokens cannot be empty")));
1045    }
1046
1047    #[tokio::test]
1048    #[serial]
1049    async fn test_validate_with_result_fee_and_any_spl_token_allowed() {
1050        let config = Config {
1051            validation: ValidationConfig {
1052                max_allowed_lamports: 1_000_000,
1053                max_signatures: 10,
1054                allowed_programs: vec![
1055                    SYSTEM_PROGRAM_ID.to_string(),
1056                    SPL_TOKEN_PROGRAM_ID.to_string(),
1057                ],
1058                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1059                allowed_spl_paid_tokens: SplTokenConfig::All, // All tokens are allowed
1060                disallowed_accounts: vec![],
1061                price_source: PriceSource::Jupiter,
1062                fee_payer_policy: FeePayerPolicy::default(),
1063                price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1064                token_2022: Token2022Config::default(),
1065                allow_durable_transactions: false,
1066            },
1067            metrics: MetricsConfig::default(),
1068            kora: KoraConfig::default(),
1069        };
1070
1071        let _ = update_config(config);
1072
1073        let rpc_client = RpcMockBuilder::new().build();
1074
1075        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1076        assert!(result.is_ok());
1077
1078        // Check that it warns about using "All" for allowed_spl_paid_tokens
1079        let warnings = result.unwrap();
1080        assert!(warnings.iter().any(|w| w.contains("Using 'All' for allowed_spl_paid_tokens")));
1081        assert!(warnings.iter().any(|w| w.contains("volatility risk")));
1082    }
1083
1084    #[tokio::test]
1085    #[serial]
1086    async fn test_validate_with_result_paid_tokens_not_in_allowed_tokens() {
1087        let config = Config {
1088            validation: ValidationConfig {
1089                max_allowed_lamports: 1_000_000,
1090                max_signatures: 10,
1091                allowed_programs: vec![
1092                    SYSTEM_PROGRAM_ID.to_string(),
1093                    SPL_TOKEN_PROGRAM_ID.to_string(),
1094                ],
1095                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1096                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1097                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // Not in allowed_tokens
1098                ]),
1099                disallowed_accounts: vec![],
1100                price_source: PriceSource::Jupiter,
1101                fee_payer_policy: FeePayerPolicy::default(),
1102                price: PriceConfig { model: PriceModel::Free },
1103                token_2022: Token2022Config::default(),
1104                allow_durable_transactions: false,
1105            },
1106            metrics: MetricsConfig::default(),
1107            kora: KoraConfig::default(),
1108        };
1109
1110        let _ = update_config(config);
1111
1112        let rpc_client = RpcMockBuilder::new().build();
1113        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1114        assert!(result.is_err());
1115        let errors = result.unwrap_err();
1116
1117        assert!(errors.iter().any(|e| e.contains("Token EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v in allowed_spl_paid_tokens must also be in allowed_tokens")));
1118    }
1119
1120    // Helper to create a simple test that only validates programs (no tokens)
1121    fn create_program_only_config() -> Config {
1122        Config {
1123            validation: ValidationConfig {
1124                max_allowed_lamports: 1_000_000,
1125                max_signatures: 10,
1126                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1127                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()], // Required to pass basic validation
1128                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1129                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1130                ]),
1131                disallowed_accounts: vec![],
1132                price_source: PriceSource::Jupiter,
1133                fee_payer_policy: FeePayerPolicy::default(),
1134                price: PriceConfig { model: PriceModel::Free },
1135                token_2022: Token2022Config::default(),
1136                allow_durable_transactions: false,
1137            },
1138            metrics: MetricsConfig::default(),
1139            kora: KoraConfig::default(),
1140        }
1141    }
1142
1143    // Helper to create a simple test that only validates tokens (no programs)
1144    fn create_token_only_config() -> Config {
1145        Config {
1146            validation: ValidationConfig {
1147                max_allowed_lamports: 1_000_000,
1148                max_signatures: 10,
1149                allowed_programs: vec![], // No programs
1150                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1151                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), // Empty to avoid duplicate validation
1152                disallowed_accounts: vec![],
1153                price_source: PriceSource::Jupiter,
1154                fee_payer_policy: FeePayerPolicy::default(),
1155                price: PriceConfig { model: PriceModel::Free },
1156                token_2022: Token2022Config::default(),
1157                allow_durable_transactions: false,
1158            },
1159            metrics: MetricsConfig::default(),
1160            kora: KoraConfig::default(),
1161        }
1162    }
1163
1164    #[tokio::test]
1165    #[serial]
1166    async fn test_validate_with_result_rpc_validation_valid_program() {
1167        let config = create_program_only_config();
1168
1169        // Initialize global config
1170        let _ = update_config(config);
1171
1172        let rpc_client = create_mock_rpc_client_with_account(&create_mock_program_account());
1173
1174        // Test with RPC validation enabled (skip_rpc_validation = false)
1175        // The program validation should pass, but token validation will fail (AccountNotFound)
1176        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1177        assert!(result.is_err());
1178        let errors = result.unwrap_err();
1179        // Should have token validation errors (account not found), but no program validation errors
1180        assert!(errors.iter().any(|e| e.contains("Token")
1181            && e.contains("validation failed")
1182            && e.contains("not found")));
1183        assert!(!errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1184    }
1185
1186    #[tokio::test]
1187    #[serial]
1188    async fn test_validate_with_result_rpc_validation_valid_token_mint() {
1189        let config = create_token_only_config();
1190
1191        // Initialize global config
1192        let _ = update_config(config);
1193
1194        let rpc_client = create_mock_rpc_client_with_mint(6);
1195
1196        // Test with RPC validation enabled (skip_rpc_validation = false)
1197        // Token validation should pass (mock returns token mint) since we have no programs
1198        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1199        assert!(result.is_ok());
1200        // Should have warnings about no programs but no errors
1201        let warnings = result.unwrap();
1202        assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
1203    }
1204
1205    #[tokio::test]
1206    #[serial]
1207    async fn test_validate_with_result_rpc_validation_non_executable_program_fails() {
1208        let config = Config {
1209            validation: ValidationConfig {
1210                max_allowed_lamports: 1_000_000,
1211                max_signatures: 10,
1212                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1213                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1214                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1215                disallowed_accounts: vec![],
1216                price_source: PriceSource::Jupiter,
1217                fee_payer_policy: FeePayerPolicy::default(),
1218                price: PriceConfig { model: PriceModel::Free },
1219                token_2022: Token2022Config::default(),
1220                allow_durable_transactions: false,
1221            },
1222            metrics: MetricsConfig::default(),
1223            kora: KoraConfig::default(),
1224        };
1225
1226        // Initialize global config
1227        let _ = update_config(config);
1228
1229        let rpc_client = create_mock_rpc_client_with_account(&create_mock_non_executable_account());
1230
1231        // Test with RPC validation enabled (skip_rpc_validation = false)
1232        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1233        assert!(result.is_err());
1234        let errors = result.unwrap_err();
1235        assert!(errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1236    }
1237
1238    #[tokio::test]
1239    #[serial]
1240    async fn test_validate_with_result_rpc_validation_account_not_found_fails() {
1241        let config = Config {
1242            validation: ValidationConfig {
1243                max_allowed_lamports: 1_000_000,
1244                max_signatures: 10,
1245                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1246                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1247                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1248                disallowed_accounts: vec![],
1249                price_source: PriceSource::Jupiter,
1250                fee_payer_policy: FeePayerPolicy::default(),
1251                price: PriceConfig { model: PriceModel::Free },
1252                token_2022: Token2022Config::default(),
1253                allow_durable_transactions: false,
1254            },
1255            metrics: MetricsConfig::default(),
1256            kora: KoraConfig::default(),
1257        };
1258
1259        let _ = update_config(config);
1260
1261        let rpc_client = create_mock_rpc_client_account_not_found();
1262
1263        // Test with RPC validation enabled (skip_rpc_validation = false)
1264        let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1265        assert!(result.is_err());
1266        let errors = result.unwrap_err();
1267        assert!(errors.len() >= 2, "Should have validation errors for programs and tokens");
1268    }
1269
1270    #[tokio::test]
1271    #[serial]
1272    async fn test_validate_with_result_skip_rpc_validation() {
1273        let config = Config {
1274            validation: ValidationConfig {
1275                max_allowed_lamports: 1_000_000,
1276                max_signatures: 10,
1277                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1278                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1279                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1280                disallowed_accounts: vec![],
1281                price_source: PriceSource::Jupiter,
1282                fee_payer_policy: FeePayerPolicy::default(),
1283                price: PriceConfig { model: PriceModel::Free },
1284                token_2022: Token2022Config::default(),
1285                allow_durable_transactions: false,
1286            },
1287            metrics: MetricsConfig::default(),
1288            kora: KoraConfig::default(),
1289        };
1290
1291        let _ = update_config(config);
1292
1293        // Use account not found RPC client - should not matter when skipping RPC validation
1294        let rpc_client = create_mock_rpc_client_account_not_found();
1295
1296        // Test with RPC validation disabled (skip_rpc_validation = true)
1297        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1298        assert!(result.is_ok()); // Should pass because RPC validation is skipped
1299    }
1300
1301    #[tokio::test]
1302    #[serial]
1303    async fn test_validate_with_result_valid_token2022_extensions() {
1304        let config = Config {
1305            validation: ValidationConfig {
1306                max_allowed_lamports: 1_000_000,
1307                max_signatures: 10,
1308                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1309                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1310                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1311                disallowed_accounts: vec![],
1312                price_source: PriceSource::Jupiter,
1313                fee_payer_policy: FeePayerPolicy::default(),
1314                price: PriceConfig { model: PriceModel::Free },
1315                token_2022: {
1316                    let mut config = Token2022Config::default();
1317                    config.blocked_mint_extensions =
1318                        vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1319                    config.blocked_account_extensions =
1320                        vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1321                    config
1322                },
1323                allow_durable_transactions: false,
1324            },
1325            metrics: MetricsConfig::default(),
1326            kora: KoraConfig::default(),
1327        };
1328
1329        let _ = update_config(config);
1330
1331        let rpc_client = RpcClient::new_with_commitment(
1332            "http://localhost:8899".to_string(),
1333            CommitmentConfig::confirmed(),
1334        );
1335        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1336        assert!(result.is_ok());
1337    }
1338
1339    #[tokio::test]
1340    #[serial]
1341    async fn test_validate_with_result_invalid_token2022_mint_extension() {
1342        let config = Config {
1343            validation: ValidationConfig {
1344                max_allowed_lamports: 1_000_000,
1345                max_signatures: 10,
1346                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1347                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1348                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1349                disallowed_accounts: vec![],
1350                price_source: PriceSource::Jupiter,
1351                fee_payer_policy: FeePayerPolicy::default(),
1352                price: PriceConfig { model: PriceModel::Free },
1353                token_2022: {
1354                    let mut config = Token2022Config::default();
1355                    config.blocked_mint_extensions = vec!["invalid_mint_extension".to_string()];
1356                    config
1357                },
1358                allow_durable_transactions: false,
1359            },
1360            metrics: MetricsConfig::default(),
1361            kora: KoraConfig::default(),
1362        };
1363
1364        let _ = update_config(config);
1365
1366        let rpc_client = RpcClient::new_with_commitment(
1367            "http://localhost:8899".to_string(),
1368            CommitmentConfig::confirmed(),
1369        );
1370        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1371        assert!(result.is_err());
1372        let errors = result.unwrap_err();
1373        assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1374            && e.contains("Invalid mint extension name: 'invalid_mint_extension'")));
1375    }
1376
1377    #[tokio::test]
1378    #[serial]
1379    async fn test_validate_with_result_invalid_token2022_account_extension() {
1380        let config = Config {
1381            validation: ValidationConfig {
1382                max_allowed_lamports: 1_000_000,
1383                max_signatures: 10,
1384                allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1385                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1386                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1387                disallowed_accounts: vec![],
1388                price_source: PriceSource::Jupiter,
1389                fee_payer_policy: FeePayerPolicy::default(),
1390                price: PriceConfig { model: PriceModel::Free },
1391                token_2022: {
1392                    let mut config = Token2022Config::default();
1393                    config.blocked_account_extensions =
1394                        vec!["invalid_account_extension".to_string()];
1395                    config
1396                },
1397                allow_durable_transactions: false,
1398            },
1399            metrics: MetricsConfig::default(),
1400            kora: KoraConfig::default(),
1401        };
1402
1403        let _ = update_config(config);
1404
1405        let rpc_client = RpcClient::new_with_commitment(
1406            "http://localhost:8899".to_string(),
1407            CommitmentConfig::confirmed(),
1408        );
1409        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1410        assert!(result.is_err());
1411        let errors = result.unwrap_err();
1412        assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1413            && e.contains("Invalid account extension name: 'invalid_account_extension'")));
1414    }
1415
1416    #[test]
1417    fn test_validate_token2022_extensions_valid() {
1418        let mut config = Token2022Config::default();
1419        config.blocked_mint_extensions =
1420            vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1421        config.blocked_account_extensions =
1422            vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1423
1424        let result = validate_token2022_extensions(&config);
1425        assert!(result.is_ok());
1426    }
1427
1428    #[test]
1429    fn test_validate_token2022_extensions_invalid_mint_extension() {
1430        let mut config = Token2022Config::default();
1431        config.blocked_mint_extensions = vec!["invalid_extension".to_string()];
1432
1433        let result = validate_token2022_extensions(&config);
1434        assert!(result.is_err());
1435        assert!(result.unwrap_err().contains("Invalid mint extension name: 'invalid_extension'"));
1436    }
1437
1438    #[test]
1439    fn test_validate_token2022_extensions_invalid_account_extension() {
1440        let mut config = Token2022Config::default();
1441        config.blocked_account_extensions = vec!["invalid_extension".to_string()];
1442
1443        let result = validate_token2022_extensions(&config);
1444        assert!(result.is_err());
1445        assert!(result
1446            .unwrap_err()
1447            .contains("Invalid account extension name: 'invalid_extension'"));
1448    }
1449
1450    #[test]
1451    fn test_validate_token2022_extensions_empty() {
1452        let config = Token2022Config::default();
1453
1454        let result = validate_token2022_extensions(&config);
1455        assert!(result.is_ok());
1456    }
1457
1458    #[tokio::test]
1459    #[serial]
1460    async fn test_validate_with_result_fee_payer_policy_warnings() {
1461        let config = Config {
1462            validation: ValidationConfig {
1463                max_allowed_lamports: 1_000_000,
1464                max_signatures: 10,
1465                allowed_programs: vec![
1466                    SYSTEM_PROGRAM_ID.to_string(),
1467                    SPL_TOKEN_PROGRAM_ID.to_string(),
1468                    TOKEN_2022_PROGRAM_ID.to_string(),
1469                ],
1470                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1471                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1472                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1473                ]),
1474                disallowed_accounts: vec![],
1475                price_source: PriceSource::Jupiter,
1476                fee_payer_policy: FeePayerPolicy {
1477                    system: SystemInstructionPolicy {
1478                        allow_transfer: true,
1479                        allow_assign: true,
1480                        allow_create_account: true,
1481                        allow_allocate: true,
1482                        nonce: NonceInstructionPolicy {
1483                            allow_initialize: true,
1484                            allow_advance: true,
1485                            allow_withdraw: true,
1486                            allow_authorize: true,
1487                        },
1488                    },
1489                    spl_token: SplTokenInstructionPolicy {
1490                        allow_transfer: true,
1491                        allow_burn: true,
1492                        allow_close_account: true,
1493                        allow_approve: true,
1494                        allow_revoke: true,
1495                        allow_set_authority: true,
1496                        allow_mint_to: true,
1497                        allow_initialize_mint: true,
1498                        allow_initialize_account: true,
1499                        allow_initialize_multisig: true,
1500                        allow_freeze_account: true,
1501                        allow_thaw_account: true,
1502                    },
1503                    token_2022: Token2022InstructionPolicy {
1504                        allow_transfer: true,
1505                        allow_burn: true,
1506                        allow_close_account: true,
1507                        allow_approve: true,
1508                        allow_revoke: true,
1509                        allow_set_authority: true,
1510                        allow_mint_to: true,
1511                        allow_initialize_mint: true,
1512                        allow_initialize_account: true,
1513                        allow_initialize_multisig: true,
1514                        allow_freeze_account: true,
1515                        allow_thaw_account: true,
1516                    },
1517                },
1518                price: PriceConfig { model: PriceModel::Free },
1519                token_2022: Token2022Config::default(),
1520                allow_durable_transactions: false,
1521            },
1522            metrics: MetricsConfig::default(),
1523            kora: KoraConfig::default(),
1524        };
1525
1526        let _ = update_config(config.clone());
1527
1528        let rpc_client = RpcClient::new_with_commitment(
1529            "http://localhost:8899".to_string(),
1530            CommitmentConfig::confirmed(),
1531        );
1532        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1533        assert!(result.is_ok());
1534        let warnings = result.unwrap();
1535
1536        // Should have warnings for ALL enabled fee payer policy flags
1537        // System policies
1538        assert!(warnings
1539            .iter()
1540            .any(|w| w.contains("System transfers") && w.contains("allow_transfer")));
1541        assert!(warnings
1542            .iter()
1543            .any(|w| w.contains("System Assign instructions") && w.contains("allow_assign")));
1544        assert!(warnings.iter().any(|w| w.contains("System CreateAccount instructions")
1545            && w.contains("allow_create_account")));
1546        assert!(warnings
1547            .iter()
1548            .any(|w| w.contains("System Allocate instructions") && w.contains("allow_allocate")));
1549
1550        // Nonce policies
1551        assert!(warnings
1552            .iter()
1553            .any(|w| w.contains("nonce account initialization") && w.contains("allow_initialize")));
1554        assert!(warnings
1555            .iter()
1556            .any(|w| w.contains("nonce account advancement") && w.contains("allow_advance")));
1557        assert!(warnings
1558            .iter()
1559            .any(|w| w.contains("nonce account withdrawals") && w.contains("allow_withdraw")));
1560        assert!(warnings
1561            .iter()
1562            .any(|w| w.contains("nonce authority changes") && w.contains("allow_authorize")));
1563
1564        // SPL Token policies
1565        assert!(warnings
1566            .iter()
1567            .any(|w| w.contains("SPL Token transfers") && w.contains("allow_transfer")));
1568        assert!(warnings
1569            .iter()
1570            .any(|w| w.contains("SPL Token burn operations") && w.contains("allow_burn")));
1571        assert!(warnings
1572            .iter()
1573            .any(|w| w.contains("SPL Token CloseAccount") && w.contains("allow_close_account")));
1574        assert!(warnings
1575            .iter()
1576            .any(|w| w.contains("SPL Token approve") && w.contains("allow_approve")));
1577        assert!(warnings
1578            .iter()
1579            .any(|w| w.contains("SPL Token revoke") && w.contains("allow_revoke")));
1580        assert!(warnings
1581            .iter()
1582            .any(|w| w.contains("SPL Token SetAuthority") && w.contains("allow_set_authority")));
1583        assert!(warnings
1584            .iter()
1585            .any(|w| w.contains("SPL Token MintTo") && w.contains("allow_mint_to")));
1586        assert!(
1587            warnings
1588                .iter()
1589                .any(|w| w.contains("SPL Token InitializeMint")
1590                    && w.contains("allow_initialize_mint"))
1591        );
1592        assert!(warnings
1593            .iter()
1594            .any(|w| w.contains("SPL Token InitializeAccount")
1595                && w.contains("allow_initialize_account")));
1596        assert!(warnings.iter().any(|w| w.contains("SPL Token InitializeMultisig")
1597            && w.contains("allow_initialize_multisig")));
1598        assert!(warnings
1599            .iter()
1600            .any(|w| w.contains("SPL Token FreezeAccount") && w.contains("allow_freeze_account")));
1601        assert!(warnings
1602            .iter()
1603            .any(|w| w.contains("SPL Token ThawAccount") && w.contains("allow_thaw_account")));
1604
1605        // Token2022 policies
1606        assert!(warnings
1607            .iter()
1608            .any(|w| w.contains("Token2022 transfers") && w.contains("allow_transfer")));
1609        assert!(warnings
1610            .iter()
1611            .any(|w| w.contains("Token2022 burn operations") && w.contains("allow_burn")));
1612        assert!(warnings
1613            .iter()
1614            .any(|w| w.contains("Token2022 CloseAccount") && w.contains("allow_close_account")));
1615        assert!(warnings
1616            .iter()
1617            .any(|w| w.contains("Token2022 approve") && w.contains("allow_approve")));
1618        assert!(warnings
1619            .iter()
1620            .any(|w| w.contains("Token2022 revoke") && w.contains("allow_revoke")));
1621        assert!(warnings
1622            .iter()
1623            .any(|w| w.contains("Token2022 SetAuthority") && w.contains("allow_set_authority")));
1624        assert!(warnings
1625            .iter()
1626            .any(|w| w.contains("Token2022 MintTo") && w.contains("allow_mint_to")));
1627        assert!(
1628            warnings
1629                .iter()
1630                .any(|w| w.contains("Token2022 InitializeMint")
1631                    && w.contains("allow_initialize_mint"))
1632        );
1633        assert!(warnings
1634            .iter()
1635            .any(|w| w.contains("Token2022 InitializeAccount")
1636                && w.contains("allow_initialize_account")));
1637        assert!(warnings.iter().any(|w| w.contains("Token2022 InitializeMultisig")
1638            && w.contains("allow_initialize_multisig")));
1639        assert!(warnings
1640            .iter()
1641            .any(|w| w.contains("Token2022 FreezeAccount") && w.contains("allow_freeze_account")));
1642        assert!(warnings
1643            .iter()
1644            .any(|w| w.contains("Token2022 ThawAccount") && w.contains("allow_thaw_account")));
1645
1646        // Each warning should contain risk explanation
1647        let fee_payer_warnings: Vec<_> =
1648            warnings.iter().filter(|w| w.contains("Fee payer policy")).collect();
1649        for warning in fee_payer_warnings {
1650            assert!(warning.contains("Risk:"));
1651            assert!(warning.contains("Consider setting"));
1652        }
1653    }
1654
1655    #[tokio::test]
1656    #[serial]
1657    async fn test_check_token_mint_extensions_permanent_delegate() {
1658        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1659
1660        let mint_with_delegate =
1661            create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::PermanentDelegate]);
1662        let mint_pubkey = Pubkey::new_unique();
1663
1664        let rpc_client = create_mock_rpc_client_with_account(&mint_with_delegate);
1665        let mut warnings = Vec::new();
1666
1667        ConfigValidator::check_token_mint_extensions(
1668            &rpc_client,
1669            &[mint_pubkey.to_string()],
1670            &mut warnings,
1671        )
1672        .await;
1673
1674        assert_eq!(warnings.len(), 1);
1675        assert!(warnings[0].contains("PermanentDelegate extension"));
1676        assert!(warnings[0].contains(&mint_pubkey.to_string()));
1677        assert!(warnings[0].contains("Risk:"));
1678        assert!(warnings[0].contains("permanent delegate can transfer or burn tokens"));
1679    }
1680
1681    #[tokio::test]
1682    #[serial]
1683    async fn test_check_token_mint_extensions_transfer_hook() {
1684        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1685
1686        let mint_with_hook =
1687            create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::TransferHook]);
1688        let mint_pubkey = Pubkey::new_unique();
1689
1690        let rpc_client = create_mock_rpc_client_with_account(&mint_with_hook);
1691        let mut warnings = Vec::new();
1692
1693        ConfigValidator::check_token_mint_extensions(
1694            &rpc_client,
1695            &[mint_pubkey.to_string()],
1696            &mut warnings,
1697        )
1698        .await;
1699
1700        assert_eq!(warnings.len(), 1);
1701        assert!(warnings[0].contains("TransferHook extension"));
1702        assert!(warnings[0].contains(&mint_pubkey.to_string()));
1703        assert!(warnings[0].contains("Risk:"));
1704        assert!(warnings[0].contains("custom program executes on every transfer"));
1705    }
1706
1707    #[tokio::test]
1708    #[serial]
1709    async fn test_check_token_mint_extensions_both() {
1710        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1711
1712        let mint_with_both = create_mock_token2022_mint_with_extensions(
1713            6,
1714            vec![ExtensionType::PermanentDelegate, ExtensionType::TransferHook],
1715        );
1716        let mint_pubkey = Pubkey::new_unique();
1717
1718        let rpc_client = create_mock_rpc_client_with_account(&mint_with_both);
1719        let mut warnings = Vec::new();
1720
1721        ConfigValidator::check_token_mint_extensions(
1722            &rpc_client,
1723            &[mint_pubkey.to_string()],
1724            &mut warnings,
1725        )
1726        .await;
1727
1728        // Should have warnings for both extensions
1729        assert_eq!(warnings.len(), 2);
1730        assert!(warnings.iter().any(|w| w.contains("PermanentDelegate extension")));
1731        assert!(warnings.iter().any(|w| w.contains("TransferHook extension")));
1732    }
1733
1734    #[tokio::test]
1735    #[serial]
1736    async fn test_check_token_mint_extensions_no_risky_extensions() {
1737        let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1738
1739        let mint_with_safe =
1740            create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::MintCloseAuthority]);
1741        let mint_pubkey = Pubkey::new_unique();
1742
1743        let rpc_client = create_mock_rpc_client_with_account(&mint_with_safe);
1744        let mut warnings = Vec::new();
1745
1746        ConfigValidator::check_token_mint_extensions(
1747            &rpc_client,
1748            &[mint_pubkey.to_string()],
1749            &mut warnings,
1750        )
1751        .await;
1752
1753        assert_eq!(warnings.len(), 0);
1754    }
1755
1756    #[tokio::test]
1757    #[serial]
1758    async fn test_durable_transactions_warning_when_enabled() {
1759        let config = Config {
1760            validation: ValidationConfig {
1761                max_allowed_lamports: 1_000_000,
1762                max_signatures: 10,
1763                allowed_programs: vec![
1764                    SYSTEM_PROGRAM_ID.to_string(),
1765                    SPL_TOKEN_PROGRAM_ID.to_string(),
1766                ],
1767                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1768                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1769                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1770                ]),
1771                disallowed_accounts: vec![],
1772                price_source: PriceSource::Jupiter,
1773                fee_payer_policy: FeePayerPolicy::default(),
1774                price: PriceConfig::default(),
1775                token_2022: Token2022Config::default(),
1776                allow_durable_transactions: true, // Enabled - should warn
1777            },
1778            kora: KoraConfig::default(),
1779            metrics: MetricsConfig::default(),
1780        };
1781
1782        let _ = update_config(config);
1783
1784        let rpc_client = RpcMockBuilder::new().build();
1785        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1786        assert!(result.is_ok());
1787        let warnings = result.unwrap();
1788
1789        assert!(warnings.iter().any(|w| w.contains("allow_durable_transactions is enabled")));
1790        assert!(warnings.iter().any(|w| w.contains("hold signed transactions indefinitely")));
1791    }
1792
1793    #[tokio::test]
1794    #[serial]
1795    async fn test_durable_transactions_no_warning_when_disabled() {
1796        let config = Config {
1797            validation: ValidationConfig {
1798                max_allowed_lamports: 1_000_000,
1799                max_signatures: 10,
1800                allowed_programs: vec![
1801                    SYSTEM_PROGRAM_ID.to_string(),
1802                    SPL_TOKEN_PROGRAM_ID.to_string(),
1803                ],
1804                allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1805                allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1806                    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1807                ]),
1808                disallowed_accounts: vec![],
1809                price_source: PriceSource::Jupiter,
1810                fee_payer_policy: FeePayerPolicy::default(),
1811                price: PriceConfig::default(),
1812                token_2022: Token2022Config::default(),
1813                allow_durable_transactions: false, // Disabled - should not warn
1814            },
1815            kora: KoraConfig::default(),
1816            metrics: MetricsConfig::default(),
1817        };
1818
1819        let _ = update_config(config);
1820
1821        let rpc_client = RpcMockBuilder::new().build();
1822        let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1823        assert!(result.is_ok());
1824        let warnings = result.unwrap();
1825
1826        assert!(!warnings.iter().any(|w| w.contains("allow_durable_transactions")));
1827    }
1828}