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