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