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 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, };
42
43 let account: Account = match rpc_client.get_account(&token_pubkey).await {
44 Ok(acc) => acc,
45 Err(_) => continue, };
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, };
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 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 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 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 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 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 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 if config.validation.max_signatures == 0 {
300 warnings.push("Max signatures is 0 - this will block all transactions".to_string());
301 }
302
303 if matches!(config.validation.price_source, PriceSource::Mock) {
305 warnings.push("Using Mock price source - not suitable for production".to_string());
306 }
307
308 if config.validation.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 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 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 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 if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
349 errors.push(format!("Invalid disallowed account address: {e}"));
350 }
351
352 if let Err(e) = validate_token2022_extensions(&config.validation.token_2022) {
354 errors.push(format!("Token2022 extension validation failed: {e}"));
355 }
356
357 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 let fees_enabled = !matches!(config.validation.price.model, PriceModel::Free);
372
373 if fees_enabled {
374 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 !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 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 Self::validate_fee_payer_policy(&config.validation.fee_payer_policy, &mut warnings);
409
410 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 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 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 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 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 if !skip_rpc_validation {
478 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 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 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 Self::check_token_mint_extensions(
520 rpc_client,
521 &config.validation.allowed_tokens,
522 &mut warnings,
523 )
524 .await;
525
526 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 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 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
590fn validate_token2022_extensions(config: &Token2022Config) -> Result<(), String> {
592 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 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 let _ = update_config(config.clone());
665
666 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 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 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, max_signatures: 0, allowed_programs: vec![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
729 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
730 disallowed_accounts: vec![],
731 price_source: PriceSource::Mock, fee_payer_policy: FeePayerPolicy::default(),
733 price: PriceConfig { model: PriceModel::Free },
734 token_2022: Token2022Config::default(),
735 },
736 kora: KoraConfig {
737 rate_limit: 0, 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 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()], 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 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![], allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
827 "invalid_token_address".to_string()
828 ]), disallowed_accounts: vec!["invalid_account_address".to_string()], price_source: PriceSource::Jupiter,
831 fee_payer_policy: FeePayerPolicy::default(),
832 price: PriceConfig {
833 model: PriceModel::Margin { margin: -0.1 }, },
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, token: "invalid_token_address".to_string(), 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(), 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, 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()], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
999 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), 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, 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 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(), ]),
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 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()], 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 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![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1127 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), 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 let _ = update_config(config);
1146
1147 let rpc_client = create_mock_rpc_client_with_account(&create_mock_program_account());
1148
1149 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1152 assert!(result.is_err());
1153 let errors = result.unwrap_err();
1154 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 let _ = update_config(config);
1168
1169 let rpc_client = create_mock_rpc_client_with_mint(6);
1170
1171 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1174 assert!(result.is_ok());
1175 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 let _ = update_config(config);
1202
1203 let rpc_client = create_mock_rpc_client_with_account(&create_mock_non_executable_account());
1204
1205 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 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 let rpc_client = create_mock_rpc_client_account_not_found();
1267
1268 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1270 assert!(result.is_ok()); }
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 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 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 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 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 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 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}