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