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