1use std::{path::Path, str::FromStr};
2
3use crate::{
4 admin::token_util::find_missing_atas,
5 config::{FeePayerPolicy, SplTokenConfig, Token2022Config},
6 constant::{LIGHTHOUSE_PROGRAM_ID, MAX_RECAPTCHA_SCORE, MIN_RECAPTCHA_SCORE},
7 fee::price::PriceModel,
8 oracle::PriceSource,
9 plugin::TransactionPluginRunner,
10 signer::SignerPoolConfig,
11 state::get_config,
12 token::{spl_token_2022_util, token::TokenUtil},
13 validator::{
14 account_validator::{validate_account, AccountType},
15 cache_validator::CacheValidator,
16 signer_validator::SignerValidator,
17 },
18 KoraError,
19};
20use solana_client::nonblocking::rpc_client::RpcClient;
21use solana_sdk::{account::Account, pubkey::Pubkey};
22use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
23use spl_token_2022_interface::{
24 extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions},
25 state::Mint as Token2022MintState,
26 ID as TOKEN_2022_PROGRAM_ID,
27};
28use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID;
29
30pub struct ConfigValidator {}
31
32impl ConfigValidator {
33 async fn check_token_mint_extensions(
35 rpc_client: &RpcClient,
36 allowed_tokens: &[String],
37 warnings: &mut Vec<String>,
38 ) {
39 for token_str in allowed_tokens {
40 let token_pubkey = match Pubkey::from_str(token_str) {
41 Ok(pk) => pk,
42 Err(_) => continue, };
44
45 let account: Account = match rpc_client.get_account(&token_pubkey).await {
46 Ok(acc) => acc,
47 Err(_) => continue, };
49
50 if account.owner != TOKEN_2022_PROGRAM_ID {
51 continue;
52 }
53
54 let mint_with_extensions =
55 match StateWithExtensions::<Token2022MintState>::unpack(&account.data) {
56 Ok(m) => m,
57 Err(_) => continue, };
59
60 if mint_with_extensions
61 .get_extension::<spl_token_2022_interface::extension::permanent_delegate::PermanentDelegate>()
62 .is_ok()
63 {
64 warnings.push(format!(
65 "⚠️ SECURITY: Token {} has PermanentDelegate extension. \
66 Risk: The permanent delegate can transfer or burn tokens at any time without owner approval. \
67 This creates significant risks for payment tokens as funds can be seized after payment. \
68 Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
69 token_str
70 ));
71 }
72
73 if mint_with_extensions
74 .get_extension::<spl_token_2022_interface::extension::transfer_hook::TransferHook>()
75 .is_ok()
76 {
77 warnings.push(format!(
78 "⚠️ SECURITY: Token {} has TransferHook extension. \
79 Risk: A custom program executes on every transfer which can reject transfers \
80 or introduce external dependencies and attack surface. \
81 Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
82 token_str
83 ));
84 }
85 }
86 }
87
88 fn validate_fee_payer_policy(policy: &FeePayerPolicy, warnings: &mut Vec<String>) {
90 macro_rules! check_fee_payer_policy {
91 ($($category:ident, $field:ident, $description:expr, $risk:expr);* $(;)?) => {
92 $(
93 if policy.$category.$field {
94 warnings.push(format!(
95 "⚠️ SECURITY: Fee payer policy allows {} ({}). \
96 Risk: {}. \
97 Consider setting [validation.fee_payer_policy.{}] {}=false to prevent abuse.",
98 $description,
99 stringify!($field),
100 $risk,
101 stringify!($category),
102 stringify!($field)
103 ));
104 }
105 )*
106 };
107 }
108
109 check_fee_payer_policy! {
110 system, allow_transfer, "System transfers",
111 "Users can make the fee payer transfer arbitrary SOL amounts. This can drain your fee payer account";
112
113 system, allow_assign, "System Assign instructions",
114 "Users can make the fee payer reassign ownership of its accounts. This can compromise account control";
115
116 system, allow_create_account, "System CreateAccount instructions",
117 "Users can make the fee payer pay for arbitrary account creations. This can drain your fee payer account";
118
119 system, allow_allocate, "System Allocate instructions",
120 "Users can make the fee payer allocate space for accounts. This can be used to waste resources";
121
122 spl_token, allow_transfer, "SPL Token transfers",
123 "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
124
125 spl_token, allow_burn, "SPL Token burn operations",
126 "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
127
128 spl_token, allow_close_account, "SPL Token CloseAccount instructions",
129 "Users can make the fee payer close token accounts. This can disrupt operations and drain fee payer";
130
131 spl_token, allow_approve, "SPL Token approve operations",
132 "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
133
134 spl_token, allow_revoke, "SPL Token revoke operations",
135 "Users can make the fee payer revoke delegates. This can disrupt authorized operations";
136
137 spl_token, allow_set_authority, "SPL Token SetAuthority instructions",
138 "Users can make the fee payer transfer authority. This can lead to complete loss of control";
139
140 spl_token, allow_mint_to, "SPL Token MintTo operations",
141 "Users can make the fee payer mint tokens. This can inflate token supply";
142
143 spl_token, allow_initialize_mint, "SPL Token InitializeMint instructions",
144 "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
145
146 spl_token, allow_initialize_account, "SPL Token InitializeAccount instructions",
147 "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
148
149 spl_token, allow_initialize_multisig, "SPL Token InitializeMultisig instructions",
150 "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
151
152 spl_token, allow_freeze_account, "SPL Token FreezeAccount instructions",
153 "Users can make the fee payer freeze token accounts. This can disrupt token operations";
154
155 spl_token, allow_thaw_account, "SPL Token ThawAccount instructions",
156 "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
157
158 token_2022, allow_transfer, "Token2022 transfers",
159 "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
160
161 token_2022, allow_burn, "Token2022 burn operations",
162 "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
163
164 token_2022, allow_close_account, "Token2022 CloseAccount instructions",
165 "Users can make the fee payer close token accounts. This can disrupt operations";
166
167 token_2022, allow_approve, "Token2022 approve operations",
168 "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
169
170 token_2022, allow_revoke, "Token2022 revoke operations",
171 "Users can make the fee payer revoke delegates. This can disrupt authorized operations";
172
173 token_2022, allow_set_authority, "Token2022 SetAuthority instructions",
174 "Users can make the fee payer transfer authority. This can lead to complete loss of control";
175
176 token_2022, allow_mint_to, "Token2022 MintTo operations",
177 "Users can make the fee payer mint tokens. This can inflate token supply";
178
179 token_2022, allow_initialize_mint, "Token2022 InitializeMint instructions",
180 "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
181
182 token_2022, allow_initialize_account, "Token2022 InitializeAccount instructions",
183 "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
184
185 token_2022, allow_initialize_multisig, "Token2022 InitializeMultisig instructions",
186 "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
187
188 token_2022, allow_freeze_account, "Token2022 FreezeAccount instructions",
189 "Users can make the fee payer freeze token accounts. This can disrupt token operations";
190
191 token_2022, allow_thaw_account, "Token2022 ThawAccount instructions",
192 "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
193 }
194
195 macro_rules! check_nonce_policy {
197 ($($field:ident, $description:expr, $risk:expr);* $(;)?) => {
198 $(
199 if policy.system.nonce.$field {
200 warnings.push(format!(
201 "⚠️ SECURITY: Fee payer policy allows {} (nonce.{}). \
202 Risk: {}. \
203 Consider setting [validation.fee_payer_policy.system.nonce] {}=false to prevent abuse.",
204 $description,
205 stringify!($field),
206 $risk,
207 stringify!($field)
208 ));
209 }
210 )*
211 };
212 }
213
214 check_nonce_policy! {
215 allow_initialize, "nonce account initialization",
216 "Users can make the fee payer the authority of nonce accounts. This can create unexpected control relationships";
217
218 allow_advance, "nonce account advancement",
219 "Users can make the fee payer advance nonce accounts. This can be used to manipulate nonce states";
220
221 allow_withdraw, "nonce account withdrawals",
222 "Users can make the fee payer withdraw from nonce accounts. This can drain nonce account balances";
223
224 allow_authorize, "nonce authority changes",
225 "Users can make the fee payer transfer nonce authority. This can lead to loss of control over nonce accounts";
226 }
227 }
228
229 pub async fn validate(_rpc_client: &RpcClient) -> Result<(), KoraError> {
230 let config = &get_config()?;
231
232 if config.validation.allowed_tokens.is_empty() {
233 return Err(KoraError::InternalServerError("No tokens enabled".to_string()));
234 }
235
236 TokenUtil::check_valid_tokens(&config.validation.allowed_tokens)?;
237
238 if let Some(payment_address) = &config.kora.payment_address {
239 if let Err(e) = Pubkey::from_str(payment_address) {
240 return Err(KoraError::InternalServerError(format!(
241 "Invalid payment address: {e}"
242 )));
243 }
244 }
245
246 Ok(())
247 }
248
249 pub async fn validate_with_result(
250 rpc_client: &RpcClient,
251 skip_rpc_validation: bool,
252 ) -> Result<Vec<String>, Vec<String>> {
253 Self::validate_with_result_and_signers(rpc_client, skip_rpc_validation, None::<&Path>).await
254 }
255}
256
257impl ConfigValidator {
258 pub async fn validate_with_result_and_signers<P: AsRef<Path>>(
259 rpc_client: &RpcClient,
260 skip_rpc_validation: bool,
261 signers_config_path: Option<P>,
262 ) -> Result<Vec<String>, Vec<String>> {
263 let mut errors = Vec::new();
264 let mut warnings = Vec::new();
265
266 let config = match get_config() {
267 Ok(c) => c,
268 Err(e) => {
269 errors.push(format!("Failed to get config: {e}"));
270 return Err(errors);
271 }
272 };
273
274 if config.kora.rate_limit == 0 {
276 warnings.push("Rate limit is set to 0 - this will block all requests".to_string());
277 }
278
279 if let Some(payment_address) = &config.kora.payment_address {
281 if let Err(e) = Pubkey::from_str(payment_address) {
282 errors.push(format!("Invalid payment address: {e}"));
283 }
284 }
285
286 let score_threshold = config.kora.auth.recaptcha_score_threshold;
288 if !(MIN_RECAPTCHA_SCORE..=MAX_RECAPTCHA_SCORE).contains(&score_threshold) {
289 errors.push(format!(
290 "recaptcha_score_threshold must be between {MIN_RECAPTCHA_SCORE} and {MAX_RECAPTCHA_SCORE}, got: {score_threshold}"
291 ));
292 }
293
294 let methods = &config.kora.enabled_methods;
296 if !methods.iter().any(|enabled| enabled) {
297 warnings.push(
298 "All rpc methods are disabled - this will block all functionality".to_string(),
299 );
300 }
301
302 let mut unique_plugins = std::collections::HashSet::new();
303 for plugin in &config.kora.plugins.enabled {
304 if !unique_plugins.insert(plugin.clone()) {
305 warnings.push(format!("Duplicate transaction plugin configured: {:?}", plugin));
306 }
307 }
308
309 let (plugin_errors, plugin_warnings) = TransactionPluginRunner::validate_config(config);
310 errors.extend(plugin_errors);
311 warnings.extend(plugin_warnings);
312
313 if config.validation.max_allowed_lamports == 0 {
315 warnings
316 .push("Max allowed lamports is 0 - this will block all SOL transfers".to_string());
317 }
318
319 if config.validation.max_signatures == 0 {
321 warnings.push("Max signatures is 0 - this will block all transactions".to_string());
322 }
323
324 if matches!(config.validation.price_source, PriceSource::Mock) {
326 warnings.push("Using Mock price source - not suitable for production".to_string());
327 }
328
329 if matches!(config.validation.price_source, PriceSource::Jupiter)
331 && std::env::var("JUPITER_API_KEY").is_err()
332 {
333 errors.push(
334 "JUPITER_API_KEY environment variable not set. Required when price_source = Jupiter".to_string()
335 );
336 }
337
338 if config.validation.allow_durable_transactions {
339 warnings.push(
340 "⚠️ SECURITY: allow_durable_transactions is enabled. \
341 Risk: Users can hold signed transactions indefinitely and execute them much later. \
342 Token values may change, your fee payer may run low on funds, or you may no longer \
343 want to subsidize these transactions. \
344 Consider disabling durable transactions unless specifically required."
345 .to_string(),
346 );
347 }
348
349 if config.validation.allowed_programs.is_empty() {
351 warnings.push(
352 "No allowed programs configured - this will block all transactions".to_string(),
353 );
354 } else {
355 if !config.validation.allowed_programs.contains(&SYSTEM_PROGRAM_ID.to_string()) {
356 warnings.push("Missing System Program in allowed programs - SOL transfers and account operations will be blocked".to_string());
357 }
358 if !config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string())
359 && !config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string())
360 {
361 warnings.push("Missing Token Program in allowed programs - SPL token operations will be blocked".to_string());
362 }
363 }
364
365 if config.kora.lighthouse.enabled {
367 let lighthouse_program = LIGHTHOUSE_PROGRAM_ID.to_string();
368 if !config.validation.allowed_programs.contains(&lighthouse_program) {
369 errors.push(format!(
370 "Lighthouse is enabled but {} is not in allowed_programs. Consider adding it to allowed_programs.",
371 LIGHTHOUSE_PROGRAM_ID
372 ));
373 }
374 if config.validation.disallowed_accounts.contains(&lighthouse_program) {
375 errors.push(format!(
376 "Lighthouse is enabled but {} is in disallowed_accounts. Consider removing it from disallowed_accounts.",
377 LIGHTHOUSE_PROGRAM_ID
378 ));
379 }
380
381 let enabled_methods = &config.kora.enabled_methods;
383 let mut unprotected_methods = Vec::new();
384 if enabled_methods.sign_and_send_transaction {
385 unprotected_methods.push("signAndSendTransaction");
386 }
387 if enabled_methods.sign_and_send_bundle {
388 unprotected_methods.push("signAndSendBundle");
389 }
390 if !unprotected_methods.is_empty() {
391 warnings.push(format!(
392 "Lighthouse is enabled but {} will NOT have fee payer protection. \
393 These methods send transactions directly to the network, so adding assertions \
394 would invalidate client signatures. Consider using signTransaction/signBundle instead.",
395 unprotected_methods.join(", ")
396 ));
397 }
398 }
399
400 for pubkey_str in &config.validation.allowed_programs {
402 if Pubkey::from_str(pubkey_str).is_err() {
403 errors.push(format!(
404 "Invalid base58 pubkey format in allowed_programs: '{}'",
405 pubkey_str
406 ));
407 }
408 }
409
410 if config.validation.allowed_tokens.is_empty() {
412 errors.push("No allowed tokens configured".to_string());
413 } else if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.allowed_tokens) {
414 errors.push(format!("Invalid token address: {e}"));
415 }
416
417 if let Err(e) =
419 TokenUtil::check_valid_tokens(config.validation.allowed_spl_paid_tokens.as_slice())
420 {
421 errors.push(format!("Invalid spl paid token address: {e}"));
422 }
423
424 if matches!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All) {
426 warnings.push(
427 "⚠️ Using 'All' for allowed_spl_paid_tokens - this accepts ANY SPL token for payment. \
428 Consider using an explicit allowlist to reduce volatility risk and protect against \
429 potentially malicious or worthless tokens being used for fees.".to_string()
430 );
431 }
432
433 if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
435 errors.push(format!("Invalid disallowed account address: {e}"));
436 }
437
438 if let Err(e) = validate_token2022_extensions(&config.validation.token_2022) {
440 errors.push(format!("Token2022 extension validation failed: {e}"));
441 }
442
443 if !config.validation.token_2022.is_mint_extension_blocked(ExtensionType::PermanentDelegate)
445 {
446 warnings.push(
447 "⚠️ SECURITY: PermanentDelegate extension is NOT blocked. Tokens with this extension \
448 allow the delegate to transfer/burn tokens at any time without owner approval. \
449 This creates significant risks:\n\
450 - Payment tokens: Funds can be seized after payment\n\
451 Consider adding \"permanent_delegate\" to blocked_mint_extensions in [validation.token2022] \
452 unless explicitly needed for your use case.".to_string()
453 );
454 }
455
456 let fees_enabled = !matches!(config.validation.price.model, PriceModel::Free);
458
459 if fees_enabled {
460 let has_token_program =
462 config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string());
463 let has_token22_program =
464 config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string());
465
466 if !has_token_program && !has_token22_program {
467 errors.push("When fees are enabled, at least one token program (SPL Token or Token2022) must be in allowed_programs".to_string());
468 }
469
470 if !config.validation.allowed_spl_paid_tokens.has_tokens() {
472 errors.push(
473 "When fees are enabled, allowed_spl_paid_tokens cannot be empty".to_string(),
474 );
475 }
476 } else {
477 warnings.push(
478 "⚠️ SECURITY: Free pricing model enabled - all transactions will be processed \
479 without charging fees."
480 .to_string(),
481 );
482 }
483
484 for paid_token in &config.validation.allowed_spl_paid_tokens {
486 if !config.validation.allowed_tokens.contains(paid_token) {
487 errors.push(format!(
488 "Token {paid_token} in allowed_spl_paid_tokens must also be in allowed_tokens"
489 ));
490 }
491 }
492
493 Self::validate_fee_payer_policy(&config.validation.fee_payer_policy, &mut warnings);
495
496 match &config.validation.price.model {
498 PriceModel::Fixed { amount, token, strict } => {
499 if *amount == 0 {
500 warnings
501 .push("Fixed price amount is 0 - transactions will be free".to_string());
502 }
503 if Pubkey::from_str(token).is_err() {
504 errors.push(format!("Invalid token address for fixed price: {token}"));
505 }
506 if !config.validation.supports_token(token) {
507 errors.push(format!(
508 "Token address for fixed price is not in allowed spl paid tokens: {token}"
509 ));
510 }
511
512 let has_auth =
514 config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
515 if !has_auth {
516 warnings.push(
517 "⚠️ SECURITY: Fixed pricing with NO authentication enabled. \
518 Without authentication, anyone can spam transactions at your expense. \
519 Consider enabling api_key or hmac_secret in [kora.auth]."
520 .to_string(),
521 );
522 }
523
524 if *strict {
526 warnings.push(
527 "Strict pricing mode enabled. \
528 Transactions where fee payer outflow exceeds the fixed price will be rejected."
529 .to_string(),
530 );
531 }
532 }
533 PriceModel::Margin { margin } => {
534 if *margin < 0.0 {
535 errors.push("Margin cannot be negative".to_string());
536 } else if *margin > 1.0 {
537 warnings.push(format!("Margin is {}% - this is very high", margin * 100.0));
538 }
539 }
540 _ => {}
541 };
542
543 let has_auth = config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
545 if !has_auth {
546 warnings.push(
547 "⚠️ SECURITY: No authentication configured (neither api_key nor hmac_secret). \
548 Authentication is strongly recommended for production deployments. \
549 Consider enabling api_key or hmac_secret in [kora.auth]."
550 .to_string(),
551 );
552 }
553
554 let usage_config = &config.kora.usage_limit;
556 if usage_config.enabled {
557 let (usage_errors, usage_warnings) = CacheValidator::validate(usage_config).await;
558 errors.extend(usage_errors);
559 warnings.extend(usage_warnings);
560 }
561
562 if config.kora.cache.enabled {
564 let (cache_errors, cache_warnings) =
565 CacheValidator::validate_rpc_cache(&config.kora.cache).await;
566 errors.extend(cache_errors);
567 warnings.extend(cache_warnings);
568 }
569
570 if !skip_rpc_validation {
572 for program_str in &config.validation.allowed_programs {
574 if let Ok(program_pubkey) = Pubkey::from_str(program_str) {
575 if let Err(e) = validate_account(
576 config,
577 rpc_client,
578 &program_pubkey,
579 Some(AccountType::Program),
580 )
581 .await
582 {
583 errors.push(format!("Program {program_str} validation failed: {e}"));
584 }
585 }
586 }
587
588 for token_str in &config.validation.allowed_tokens {
590 if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
591 if let Err(e) =
592 validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
593 .await
594 {
595 errors.push(format!("Token {token_str} validation failed: {e}"));
596 }
597 }
598 }
599
600 for token_str in &config.validation.allowed_spl_paid_tokens {
602 if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
603 if let Err(e) =
604 validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
605 .await
606 {
607 errors.push(format!("SPL paid token {token_str} validation failed: {e}"));
608 }
609 }
610 }
611
612 Self::check_token_mint_extensions(
614 rpc_client,
615 &config.validation.allowed_tokens,
616 &mut warnings,
617 )
618 .await;
619
620 if let Some(payment_address) = &config.kora.payment_address {
622 if let Ok(payment_address) = Pubkey::from_str(payment_address) {
623 match find_missing_atas(config, rpc_client, &payment_address).await {
624 Ok(atas_to_create) => {
625 if !atas_to_create.is_empty() {
626 errors.push(format!(
627 "Missing ATAs for payment address: {payment_address}"
628 ));
629 }
630 }
631 Err(e) => errors.push(format!("Failed to find missing ATAs: {e}")),
632 }
633 } else {
634 errors.push(format!("Invalid payment address: {payment_address}"));
635 }
636 }
637 }
638
639 if let Some(path) = signers_config_path {
641 match SignerPoolConfig::load_config(path.as_ref()) {
642 Ok(signer_config) => {
643 let (signer_warnings, signer_errors) =
644 SignerValidator::validate_with_result(&signer_config);
645 warnings.extend(signer_warnings);
646 errors.extend(signer_errors);
647 }
648 Err(e) => {
649 errors.push(format!("Failed to load signers config: {e}"));
650 }
651 }
652 } else {
653 println!("ℹ️ Signers configuration not validated. Include --signers-config path/to/signers.toml to validate signers");
654 }
655
656 println!("=== Configuration Validation ===");
658 if errors.is_empty() {
659 println!("✓ Configuration validation successful!");
660 } else {
661 println!("✗ Configuration validation failed!");
662 println!("\n❌ Errors:");
663 for error in &errors {
664 println!(" - {error}");
665 }
666 println!("\nPlease fix the configuration errors above before deploying.");
667 }
668
669 if !warnings.is_empty() {
670 println!("\n⚠️ Warnings:");
671 for warning in &warnings {
672 println!(" - {warning}");
673 }
674 }
675
676 if errors.is_empty() {
677 Ok(warnings)
678 } else {
679 Err(errors)
680 }
681 }
682}
683
684fn validate_token2022_extensions(config: &Token2022Config) -> Result<(), String> {
686 for ext_name in &config.blocked_mint_extensions {
688 if spl_token_2022_util::parse_mint_extension_string(ext_name).is_none() {
689 return Err(format!(
690 "Invalid mint extension name: '{ext_name}'. Valid names are: {:?}",
691 spl_token_2022_util::get_all_mint_extension_names()
692 ));
693 }
694 }
695
696 for ext_name in &config.blocked_account_extensions {
698 if spl_token_2022_util::parse_account_extension_string(ext_name).is_none() {
699 return Err(format!(
700 "Invalid account extension name: '{ext_name}'. Valid names are: {:?}",
701 spl_token_2022_util::get_all_account_extension_names()
702 ));
703 }
704 }
705
706 Ok(())
707}
708
709#[cfg(test)]
710mod tests {
711 use crate::{
712 config::{
713 AuthConfig, BundleConfig, CacheConfig, Config, EnabledMethods, FeePayerPolicy,
714 KoraConfig, LighthouseConfig, MetricsConfig, NonceInstructionPolicy, PluginsConfig,
715 SplTokenConfig, SplTokenInstructionPolicy, SystemInstructionPolicy,
716 Token2022InstructionPolicy, TransactionPluginType, UsageLimitConfig, ValidationConfig,
717 },
718 constant::{DEFAULT_MAX_REQUEST_BODY_SIZE, LIGHTHOUSE_PROGRAM_ID},
719 fee::price::PriceConfig,
720 state::update_config,
721 tests::{
722 account_mock::create_mock_token2022_mint_with_extensions,
723 common::{
724 create_mock_non_executable_account, create_mock_program_account,
725 create_mock_rpc_client_account_not_found, create_mock_rpc_client_with_account,
726 create_mock_rpc_client_with_mint, RpcMockBuilder,
727 },
728 config_mock::ConfigMockBuilder,
729 },
730 };
731 use serial_test::serial;
732 use solana_commitment_config::CommitmentConfig;
733 use spl_token_2022_interface::extension::ExtensionType;
734
735 use super::*;
736
737 #[tokio::test]
738 #[serial]
739 async fn test_validate_config() {
740 let mut config = Config {
741 validation: ValidationConfig {
742 max_allowed_lamports: 1000000000,
743 max_signatures: 10,
744 allowed_programs: vec!["program1".to_string()],
745 allowed_tokens: vec!["token1".to_string()],
746 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec!["token3".to_string()]),
747 disallowed_accounts: vec!["account1".to_string()],
748 price_source: PriceSource::Jupiter,
749 fee_payer_policy: FeePayerPolicy::default(),
750 price: PriceConfig::default(),
751 token_2022: Token2022Config::default(),
752 allow_durable_transactions: false,
753 max_price_staleness_slots: 0,
754 },
755 kora: KoraConfig::default(),
756 metrics: MetricsConfig::default(),
757 };
758
759 let _ = update_config(config.clone());
761
762 config.validation.allowed_tokens = vec![];
764 let _ = update_config(config);
765
766 let rpc_client = RpcClient::new_with_commitment(
767 "http://localhost:8899".to_string(),
768 CommitmentConfig::confirmed(),
769 );
770 let result = ConfigValidator::validate(&rpc_client).await;
771 assert!(result.is_err());
772 assert!(matches!(result.unwrap_err(), KoraError::InternalServerError(_)));
773 }
774
775 #[tokio::test]
776 #[serial]
777 async fn test_validate_with_result_successful_config() {
778 std::env::set_var("JUPITER_API_KEY", "test-api-key");
779 let config = Config {
780 validation: ValidationConfig {
781 max_allowed_lamports: 1_000_000,
782 max_signatures: 10,
783 allowed_programs: vec![
784 SYSTEM_PROGRAM_ID.to_string(),
785 SPL_TOKEN_PROGRAM_ID.to_string(),
786 ],
787 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
788 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
789 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
790 ]),
791 disallowed_accounts: vec![],
792 price_source: PriceSource::Jupiter,
793 fee_payer_policy: FeePayerPolicy::default(),
794 price: PriceConfig::default(),
795 token_2022: Token2022Config::default(),
796 allow_durable_transactions: false,
797 max_price_staleness_slots: 0,
798 },
799 kora: KoraConfig::default(),
800 metrics: MetricsConfig::default(),
801 };
802
803 let _ = update_config(config);
805
806 let rpc_client = RpcClient::new_with_commitment(
807 "http://localhost:8899".to_string(),
808 CommitmentConfig::confirmed(),
809 );
810 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
811 assert!(result.is_ok());
812 let warnings = result.unwrap();
813 assert_eq!(warnings.len(), 2);
815 assert!(warnings.iter().any(|w| w.contains("PermanentDelegate")));
816 assert!(warnings.iter().any(|w| w.contains("No authentication configured")));
817 }
818
819 #[tokio::test]
820 #[serial]
821 async fn test_validate_with_result_warnings() {
822 let config = Config {
823 validation: ValidationConfig {
824 max_allowed_lamports: 0, max_signatures: 0, allowed_programs: vec![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
828 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
829 disallowed_accounts: vec![],
830 price_source: PriceSource::Mock, fee_payer_policy: FeePayerPolicy::default(),
832 price: PriceConfig { model: PriceModel::Free },
833 token_2022: Token2022Config::default(),
834 allow_durable_transactions: false,
835 max_price_staleness_slots: 0,
836 },
837 kora: KoraConfig {
838 rate_limit: 0, max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
840 enabled_methods: EnabledMethods {
841 liveness: false,
842 estimate_transaction_fee: false,
843 get_supported_tokens: false,
844 sign_transaction: false,
845 sign_and_send_transaction: false,
846 transfer_transaction: false,
847 get_blockhash: false,
848 get_config: false,
849 get_payer_signer: false,
850 get_version: false,
851 estimate_bundle_fee: false,
852 sign_and_send_bundle: false,
853 sign_bundle: false,
854 },
855 auth: AuthConfig::default(),
856 payment_address: None,
857 cache: CacheConfig::default(),
858 usage_limit: UsageLimitConfig::default(),
859 plugins: PluginsConfig::default(),
860 bundle: BundleConfig::default(),
861 lighthouse: LighthouseConfig::default(),
862 force_sig_verify: false,
863 },
864 metrics: MetricsConfig::default(),
865 };
866
867 let _ = update_config(config);
869
870 let rpc_client = RpcClient::new_with_commitment(
871 "http://localhost:8899".to_string(),
872 CommitmentConfig::confirmed(),
873 );
874 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
875 assert!(result.is_ok());
876 let warnings = result.unwrap();
877
878 assert!(!warnings.is_empty());
879 assert!(warnings.iter().any(|w| w.contains("Rate limit is set to 0")));
880 assert!(warnings.iter().any(|w| w.contains("All rpc methods are disabled")));
881 assert!(warnings.iter().any(|w| w.contains("Max allowed lamports is 0")));
882 assert!(warnings.iter().any(|w| w.contains("Max signatures is 0")));
883 assert!(warnings.iter().any(|w| w.contains("Using Mock price source")));
884 assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
885 }
886
887 #[tokio::test]
888 #[serial]
889 async fn test_validate_with_result_duplicate_plugins_warn() {
890 let mut config = ConfigMockBuilder::new().build();
891 config.kora.cache.enabled = false;
892 config.kora.plugins.enabled =
893 vec![TransactionPluginType::GasSwap, TransactionPluginType::GasSwap];
894
895 let _ = update_config(config);
896
897 let rpc_client = RpcMockBuilder::new().build();
898 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
899 assert!(result.is_ok());
900 let warnings = result.unwrap();
901
902 assert!(warnings.iter().any(|w| w.contains("Duplicate transaction plugin configured")));
903 }
904
905 #[tokio::test]
906 #[serial]
907 async fn test_gas_swap_plugin_requires_system_program() {
908 let mut config = ConfigMockBuilder::new().build();
909 config.kora.cache.enabled = false;
910 config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
911 config.validation.allowed_programs = vec![SPL_TOKEN_PROGRAM_ID.to_string()]; let _ = update_config(config);
914
915 let rpc_client = RpcMockBuilder::new().build();
916 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
917 assert!(result.is_err());
918 assert!(result
919 .unwrap_err()
920 .iter()
921 .any(|e| e.contains("GasSwap plugin requires System Program")));
922 }
923
924 #[tokio::test]
925 #[serial]
926 async fn test_gas_swap_plugin_requires_token_program() {
927 let mut config = ConfigMockBuilder::new().build();
928 config.kora.cache.enabled = false;
929 config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
930 config.validation.allowed_programs = vec![SYSTEM_PROGRAM_ID.to_string()]; let _ = update_config(config);
933
934 let rpc_client = RpcMockBuilder::new().build();
935 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
936 assert!(result.is_err());
937 assert!(result
938 .unwrap_err()
939 .iter()
940 .any(|e| e.contains("GasSwap plugin requires at least one token program")));
941 }
942
943 #[tokio::test]
944 #[serial]
945 async fn test_gas_swap_plugin_requires_allowed_tokens() {
946 let mut config = ConfigMockBuilder::new().build();
947 config.kora.cache.enabled = false;
948 config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
949 config.validation.allowed_tokens = vec![];
950
951 let _ = update_config(config);
952
953 let rpc_client = RpcMockBuilder::new().build();
954 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
955 assert!(result.is_err());
956 assert!(result
957 .unwrap_err()
958 .iter()
959 .any(|e| e.contains("GasSwap plugin requires at least one token in allowed_tokens")));
960 }
961
962 #[tokio::test]
963 #[serial]
964 async fn test_gas_swap_plugin_rejects_free_pricing() {
965 let mut config = ConfigMockBuilder::new().build();
966 config.kora.cache.enabled = false;
967 config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
968 config.validation.price = PriceConfig { model: PriceModel::Free };
969
970 let _ = update_config(config);
971
972 let rpc_client = RpcMockBuilder::new().build();
973 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
974 assert!(result.is_err());
975 assert!(result
976 .unwrap_err()
977 .iter()
978 .any(|e| e.contains("GasSwap plugin cannot be used with Free pricing")));
979 }
980
981 #[tokio::test]
982 #[serial]
983 async fn test_validate_with_result_missing_system_program_warning() {
984 std::env::set_var("JUPITER_API_KEY", "test-api-key");
985 let config = Config {
986 validation: ValidationConfig {
987 max_allowed_lamports: 1_000_000,
988 max_signatures: 10,
989 allowed_programs: vec!["11111111111111111111111111111112".to_string()], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
991 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
992 disallowed_accounts: vec![],
993 price_source: PriceSource::Jupiter,
994 fee_payer_policy: FeePayerPolicy::default(),
995 price: PriceConfig { model: PriceModel::Free },
996 token_2022: Token2022Config::default(),
997 allow_durable_transactions: false,
998 max_price_staleness_slots: 0,
999 },
1000 kora: KoraConfig::default(),
1001 metrics: MetricsConfig::default(),
1002 };
1003
1004 let _ = update_config(config);
1006
1007 let rpc_client = RpcClient::new_with_commitment(
1008 "http://localhost:8899".to_string(),
1009 CommitmentConfig::confirmed(),
1010 );
1011 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1012 assert!(result.is_ok());
1013 let warnings = result.unwrap();
1014
1015 assert!(warnings.iter().any(|w| w.contains("Missing System Program in allowed programs")));
1016 assert!(warnings.iter().any(|w| w.contains("Missing Token Program in allowed programs")));
1017 }
1018
1019 #[tokio::test]
1020 #[serial]
1021 async fn test_validate_with_result_errors() {
1022 let config = Config {
1023 validation: ValidationConfig {
1024 max_allowed_lamports: 1_000_000,
1025 max_signatures: 10,
1026 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1027 allowed_tokens: vec![], allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1029 "invalid_token_address".to_string()
1030 ]), disallowed_accounts: vec!["invalid_account_address".to_string()], price_source: PriceSource::Jupiter,
1033 fee_payer_policy: FeePayerPolicy::default(),
1034 price: PriceConfig {
1035 model: PriceModel::Margin { margin: -0.1 }, },
1037 token_2022: Token2022Config::default(),
1038 allow_durable_transactions: false,
1039 max_price_staleness_slots: 0,
1040 },
1041 metrics: MetricsConfig::default(),
1042 kora: KoraConfig::default(),
1043 };
1044
1045 let _ = update_config(config);
1046
1047 let rpc_client = RpcClient::new_with_commitment(
1048 "http://localhost:8899".to_string(),
1049 CommitmentConfig::confirmed(),
1050 );
1051 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1052 assert!(result.is_err());
1053 let errors = result.unwrap_err();
1054
1055 assert!(errors.iter().any(|e| e.contains("No allowed tokens configured")));
1056 assert!(errors.iter().any(|e| e.contains("Invalid spl paid token address")));
1057 assert!(errors.iter().any(|e| e.contains("Invalid disallowed account address")));
1058 assert!(errors.iter().any(|e| e.contains("Margin cannot be negative")));
1059 }
1060
1061 #[tokio::test]
1062 #[serial]
1063 async fn test_validate_with_result_fixed_price_errors() {
1064 let config = Config {
1065 validation: ValidationConfig {
1066 max_allowed_lamports: 1_000_000,
1067 max_signatures: 10,
1068 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1069 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1070 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1071 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1072 ]),
1073 disallowed_accounts: vec![],
1074 price_source: PriceSource::Jupiter,
1075 fee_payer_policy: FeePayerPolicy::default(),
1076 price: PriceConfig {
1077 model: PriceModel::Fixed {
1078 amount: 0, token: "invalid_token_address".to_string(), strict: false,
1081 },
1082 },
1083 token_2022: Token2022Config::default(),
1084 allow_durable_transactions: false,
1085 max_price_staleness_slots: 0,
1086 },
1087 metrics: MetricsConfig::default(),
1088 kora: KoraConfig::default(),
1089 };
1090
1091 let _ = update_config(config);
1092
1093 let rpc_client = RpcClient::new_with_commitment(
1094 "http://localhost:8899".to_string(),
1095 CommitmentConfig::confirmed(),
1096 );
1097 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1098 assert!(result.is_err());
1099 let errors = result.unwrap_err();
1100
1101 assert!(errors.iter().any(|e| e.contains("Invalid token address for fixed price")));
1102 }
1103
1104 #[tokio::test]
1105 #[serial]
1106 async fn test_validate_with_result_fixed_price_not_in_allowed_tokens() {
1107 let config = Config {
1108 validation: ValidationConfig {
1109 max_allowed_lamports: 1_000_000,
1110 max_signatures: 10,
1111 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1112 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1113 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1114 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1115 ]),
1116 disallowed_accounts: vec![],
1117 price_source: PriceSource::Jupiter,
1118 fee_payer_policy: FeePayerPolicy::default(),
1119 price: PriceConfig {
1120 model: PriceModel::Fixed {
1121 amount: 1000,
1122 token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), strict: false,
1124 },
1125 },
1126 token_2022: Token2022Config::default(),
1127 allow_durable_transactions: false,
1128 max_price_staleness_slots: 0,
1129 },
1130 metrics: MetricsConfig::default(),
1131 kora: KoraConfig::default(),
1132 };
1133
1134 let _ = update_config(config);
1135
1136 let rpc_client = RpcClient::new_with_commitment(
1137 "http://localhost:8899".to_string(),
1138 CommitmentConfig::confirmed(),
1139 );
1140 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1141 assert!(result.is_err());
1142 let errors = result.unwrap_err();
1143
1144 assert!(
1145 errors
1146 .iter()
1147 .any(|e| e
1148 .contains("Token address for fixed price is not in allowed spl paid tokens"))
1149 );
1150 }
1151
1152 #[tokio::test]
1153 #[serial]
1154 async fn test_validate_with_result_fixed_price_zero_amount_warning() {
1155 std::env::set_var("JUPITER_API_KEY", "test-api-key");
1156 let config = Config {
1157 validation: ValidationConfig {
1158 max_allowed_lamports: 1_000_000,
1159 max_signatures: 10,
1160 allowed_programs: vec![
1161 SYSTEM_PROGRAM_ID.to_string(),
1162 SPL_TOKEN_PROGRAM_ID.to_string(),
1163 ],
1164 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1165 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1166 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1167 ]),
1168 disallowed_accounts: vec![],
1169 price_source: PriceSource::Jupiter,
1170 fee_payer_policy: FeePayerPolicy::default(),
1171 price: PriceConfig {
1172 model: PriceModel::Fixed {
1173 amount: 0, token: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1175 strict: false,
1176 },
1177 },
1178 token_2022: Token2022Config::default(),
1179 allow_durable_transactions: false,
1180 max_price_staleness_slots: 0,
1181 },
1182 metrics: MetricsConfig::default(),
1183 kora: KoraConfig::default(),
1184 };
1185
1186 let _ = update_config(config);
1187
1188 let rpc_client = RpcClient::new_with_commitment(
1189 "http://localhost:8899".to_string(),
1190 CommitmentConfig::confirmed(),
1191 );
1192 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1193 assert!(result.is_ok());
1194 let warnings = result.unwrap();
1195
1196 assert!(warnings
1197 .iter()
1198 .any(|w| w.contains("Fixed price amount is 0 - transactions will be free")));
1199 }
1200
1201 #[tokio::test]
1202 #[serial]
1203 async fn test_validate_with_result_fee_validation_errors() {
1204 let config = Config {
1205 validation: ValidationConfig {
1206 max_allowed_lamports: 1_000_000,
1207 max_signatures: 10,
1208 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1210 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), disallowed_accounts: vec![],
1212 price_source: PriceSource::Jupiter,
1213 fee_payer_policy: FeePayerPolicy::default(),
1214 price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1215 token_2022: Token2022Config::default(),
1216 allow_durable_transactions: false,
1217 max_price_staleness_slots: 0,
1218 },
1219 metrics: MetricsConfig::default(),
1220 kora: KoraConfig::default(),
1221 };
1222
1223 let _ = update_config(config);
1224
1225 let rpc_client = RpcClient::new_with_commitment(
1226 "http://localhost:8899".to_string(),
1227 CommitmentConfig::confirmed(),
1228 );
1229 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1230 assert!(result.is_err());
1231 let errors = result.unwrap_err();
1232
1233 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")));
1234 assert!(errors
1235 .iter()
1236 .any(|e| e.contains("When fees are enabled, allowed_spl_paid_tokens cannot be empty")));
1237 }
1238
1239 #[tokio::test]
1240 #[serial]
1241 async fn test_validate_with_result_fee_and_any_spl_token_allowed() {
1242 std::env::set_var("JUPITER_API_KEY", "test-api-key");
1243 let config = Config {
1244 validation: ValidationConfig {
1245 max_allowed_lamports: 1_000_000,
1246 max_signatures: 10,
1247 allowed_programs: vec![
1248 SYSTEM_PROGRAM_ID.to_string(),
1249 SPL_TOKEN_PROGRAM_ID.to_string(),
1250 ],
1251 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1252 allowed_spl_paid_tokens: SplTokenConfig::All, disallowed_accounts: vec![],
1254 price_source: PriceSource::Jupiter,
1255 fee_payer_policy: FeePayerPolicy::default(),
1256 price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1257 token_2022: Token2022Config::default(),
1258 allow_durable_transactions: false,
1259 max_price_staleness_slots: 0,
1260 },
1261 metrics: MetricsConfig::default(),
1262 kora: KoraConfig::default(),
1263 };
1264
1265 let _ = update_config(config);
1266
1267 let rpc_client = RpcMockBuilder::new().build();
1268
1269 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1270 assert!(result.is_ok());
1271
1272 let warnings = result.unwrap();
1274 assert!(warnings.iter().any(|w| w.contains("Using 'All' for allowed_spl_paid_tokens")));
1275 assert!(warnings.iter().any(|w| w.contains("volatility risk")));
1276 }
1277
1278 #[tokio::test]
1279 #[serial]
1280 async fn test_validate_with_result_paid_tokens_not_in_allowed_tokens() {
1281 let config = Config {
1282 validation: ValidationConfig {
1283 max_allowed_lamports: 1_000_000,
1284 max_signatures: 10,
1285 allowed_programs: vec![
1286 SYSTEM_PROGRAM_ID.to_string(),
1287 SPL_TOKEN_PROGRAM_ID.to_string(),
1288 ],
1289 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1290 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1291 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), ]),
1293 disallowed_accounts: vec![],
1294 price_source: PriceSource::Jupiter,
1295 fee_payer_policy: FeePayerPolicy::default(),
1296 price: PriceConfig { model: PriceModel::Free },
1297 token_2022: Token2022Config::default(),
1298 allow_durable_transactions: false,
1299 max_price_staleness_slots: 0,
1300 },
1301 metrics: MetricsConfig::default(),
1302 kora: KoraConfig::default(),
1303 };
1304
1305 let _ = update_config(config);
1306
1307 let rpc_client = RpcMockBuilder::new().build();
1308 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1309 assert!(result.is_err());
1310 let errors = result.unwrap_err();
1311
1312 assert!(errors.iter().any(|e| e.contains("Token EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v in allowed_spl_paid_tokens must also be in allowed_tokens")));
1313 }
1314
1315 fn create_program_only_config() -> Config {
1317 Config {
1318 validation: ValidationConfig {
1319 max_allowed_lamports: 1_000_000,
1320 max_signatures: 10,
1321 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1322 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()], allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1324 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1325 ]),
1326 disallowed_accounts: vec![],
1327 price_source: PriceSource::Jupiter,
1328 fee_payer_policy: FeePayerPolicy::default(),
1329 price: PriceConfig { model: PriceModel::Free },
1330 token_2022: Token2022Config::default(),
1331 allow_durable_transactions: false,
1332 max_price_staleness_slots: 0,
1333 },
1334 metrics: MetricsConfig::default(),
1335 kora: KoraConfig::default(),
1336 }
1337 }
1338
1339 fn create_token_only_config() -> Config {
1341 Config {
1342 validation: ValidationConfig {
1343 max_allowed_lamports: 1_000_000,
1344 max_signatures: 10,
1345 allowed_programs: vec![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1347 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), disallowed_accounts: vec![],
1349 price_source: PriceSource::Jupiter,
1350 fee_payer_policy: FeePayerPolicy::default(),
1351 price: PriceConfig { model: PriceModel::Free },
1352 token_2022: Token2022Config::default(),
1353 allow_durable_transactions: false,
1354 max_price_staleness_slots: 0,
1355 },
1356 metrics: MetricsConfig::default(),
1357 kora: KoraConfig::default(),
1358 }
1359 }
1360
1361 #[tokio::test]
1362 #[serial]
1363 async fn test_validate_with_result_rpc_validation_valid_program() {
1364 let config = create_program_only_config();
1365
1366 let _ = update_config(config);
1368
1369 let rpc_client = create_mock_rpc_client_with_account(&create_mock_program_account());
1370
1371 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1374 assert!(result.is_err());
1375 let errors = result.unwrap_err();
1376 assert!(errors.iter().any(|e| e.contains("Token")
1378 && e.contains("validation failed")
1379 && e.contains("not found")));
1380 assert!(!errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1381 }
1382
1383 #[tokio::test]
1384 #[serial]
1385 async fn test_validate_with_result_rpc_validation_valid_token_mint() {
1386 std::env::set_var("JUPITER_API_KEY", "test-api-key");
1387 let config = create_token_only_config();
1388
1389 let _ = update_config(config);
1391
1392 let rpc_client = create_mock_rpc_client_with_mint(6);
1393
1394 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1397 assert!(result.is_ok());
1398 let warnings = result.unwrap();
1400 assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
1401 }
1402
1403 #[tokio::test]
1404 #[serial]
1405 async fn test_validate_with_result_rpc_validation_non_executable_program_fails() {
1406 let config = Config {
1407 validation: ValidationConfig {
1408 max_allowed_lamports: 1_000_000,
1409 max_signatures: 10,
1410 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1411 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1412 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1413 disallowed_accounts: vec![],
1414 price_source: PriceSource::Jupiter,
1415 fee_payer_policy: FeePayerPolicy::default(),
1416 price: PriceConfig { model: PriceModel::Free },
1417 token_2022: Token2022Config::default(),
1418 allow_durable_transactions: false,
1419 max_price_staleness_slots: 0,
1420 },
1421 metrics: MetricsConfig::default(),
1422 kora: KoraConfig::default(),
1423 };
1424
1425 let _ = update_config(config);
1427
1428 let rpc_client = create_mock_rpc_client_with_account(&create_mock_non_executable_account());
1429
1430 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1432 assert!(result.is_err());
1433 let errors = result.unwrap_err();
1434 assert!(errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1435 }
1436
1437 #[tokio::test]
1438 #[serial]
1439 async fn test_validate_with_result_rpc_validation_account_not_found_fails() {
1440 let config = Config {
1441 validation: ValidationConfig {
1442 max_allowed_lamports: 1_000_000,
1443 max_signatures: 10,
1444 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1445 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1446 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1447 disallowed_accounts: vec![],
1448 price_source: PriceSource::Jupiter,
1449 fee_payer_policy: FeePayerPolicy::default(),
1450 price: PriceConfig { model: PriceModel::Free },
1451 token_2022: Token2022Config::default(),
1452 allow_durable_transactions: false,
1453 max_price_staleness_slots: 0,
1454 },
1455 metrics: MetricsConfig::default(),
1456 kora: KoraConfig::default(),
1457 };
1458
1459 let _ = update_config(config);
1460
1461 let rpc_client = create_mock_rpc_client_account_not_found();
1462
1463 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1465 assert!(result.is_err());
1466 let errors = result.unwrap_err();
1467 assert!(errors.len() >= 2, "Should have validation errors for programs and tokens");
1468 }
1469
1470 #[tokio::test]
1471 #[serial]
1472 async fn test_validate_with_result_skip_rpc_validation() {
1473 std::env::set_var("JUPITER_API_KEY", "test-api-key");
1474 let config = Config {
1475 validation: ValidationConfig {
1476 max_allowed_lamports: 1_000_000,
1477 max_signatures: 10,
1478 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1479 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1480 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1481 disallowed_accounts: vec![],
1482 price_source: PriceSource::Jupiter,
1483 fee_payer_policy: FeePayerPolicy::default(),
1484 price: PriceConfig { model: PriceModel::Free },
1485 token_2022: Token2022Config::default(),
1486 allow_durable_transactions: false,
1487 max_price_staleness_slots: 0,
1488 },
1489 metrics: MetricsConfig::default(),
1490 kora: KoraConfig::default(),
1491 };
1492
1493 let _ = update_config(config);
1494
1495 let rpc_client = create_mock_rpc_client_account_not_found();
1497
1498 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1500 assert!(result.is_ok()); }
1502
1503 #[tokio::test]
1504 #[serial]
1505 async fn test_validate_with_result_valid_token2022_extensions() {
1506 std::env::set_var("JUPITER_API_KEY", "test-api-key");
1507 let config = Config {
1508 validation: ValidationConfig {
1509 max_allowed_lamports: 1_000_000,
1510 max_signatures: 10,
1511 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1512 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1513 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1514 disallowed_accounts: vec![],
1515 price_source: PriceSource::Jupiter,
1516 fee_payer_policy: FeePayerPolicy::default(),
1517 price: PriceConfig { model: PriceModel::Free },
1518 token_2022: {
1519 let mut config = Token2022Config::default();
1520 config.blocked_mint_extensions =
1521 vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1522 config.blocked_account_extensions =
1523 vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1524 config
1525 },
1526 allow_durable_transactions: false,
1527 max_price_staleness_slots: 0,
1528 },
1529 metrics: MetricsConfig::default(),
1530 kora: KoraConfig::default(),
1531 };
1532
1533 let _ = update_config(config);
1534
1535 let rpc_client = RpcClient::new_with_commitment(
1536 "http://localhost:8899".to_string(),
1537 CommitmentConfig::confirmed(),
1538 );
1539 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1540 assert!(result.is_ok());
1541 }
1542
1543 #[tokio::test]
1544 #[serial]
1545 async fn test_validate_with_result_invalid_token2022_mint_extension() {
1546 let config = Config {
1547 validation: ValidationConfig {
1548 max_allowed_lamports: 1_000_000,
1549 max_signatures: 10,
1550 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1551 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1552 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1553 disallowed_accounts: vec![],
1554 price_source: PriceSource::Jupiter,
1555 fee_payer_policy: FeePayerPolicy::default(),
1556 price: PriceConfig { model: PriceModel::Free },
1557 token_2022: {
1558 let mut config = Token2022Config::default();
1559 config.blocked_mint_extensions = vec!["invalid_mint_extension".to_string()];
1560 config
1561 },
1562 allow_durable_transactions: false,
1563 max_price_staleness_slots: 0,
1564 },
1565 metrics: MetricsConfig::default(),
1566 kora: KoraConfig::default(),
1567 };
1568
1569 let _ = update_config(config);
1570
1571 let rpc_client = RpcClient::new_with_commitment(
1572 "http://localhost:8899".to_string(),
1573 CommitmentConfig::confirmed(),
1574 );
1575 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1576 assert!(result.is_err());
1577 let errors = result.unwrap_err();
1578 assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1579 && e.contains("Invalid mint extension name: 'invalid_mint_extension'")));
1580 }
1581
1582 #[tokio::test]
1583 #[serial]
1584 async fn test_validate_with_result_invalid_token2022_account_extension() {
1585 let config = Config {
1586 validation: ValidationConfig {
1587 max_allowed_lamports: 1_000_000,
1588 max_signatures: 10,
1589 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1590 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1591 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1592 disallowed_accounts: vec![],
1593 price_source: PriceSource::Jupiter,
1594 fee_payer_policy: FeePayerPolicy::default(),
1595 price: PriceConfig { model: PriceModel::Free },
1596 token_2022: {
1597 let mut config = Token2022Config::default();
1598 config.blocked_account_extensions =
1599 vec!["invalid_account_extension".to_string()];
1600 config
1601 },
1602 allow_durable_transactions: false,
1603 max_price_staleness_slots: 0,
1604 },
1605 metrics: MetricsConfig::default(),
1606 kora: KoraConfig::default(),
1607 };
1608
1609 let _ = update_config(config);
1610
1611 let rpc_client = RpcClient::new_with_commitment(
1612 "http://localhost:8899".to_string(),
1613 CommitmentConfig::confirmed(),
1614 );
1615 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1616 assert!(result.is_err());
1617 let errors = result.unwrap_err();
1618 assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1619 && e.contains("Invalid account extension name: 'invalid_account_extension'")));
1620 }
1621
1622 #[test]
1623 fn test_validate_token2022_extensions_valid() {
1624 let mut config = Token2022Config::default();
1625 config.blocked_mint_extensions =
1626 vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1627 config.blocked_account_extensions =
1628 vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1629
1630 let result = validate_token2022_extensions(&config);
1631 assert!(result.is_ok());
1632 }
1633
1634 #[test]
1635 fn test_validate_token2022_extensions_invalid_mint_extension() {
1636 let mut config = Token2022Config::default();
1637 config.blocked_mint_extensions = vec!["invalid_extension".to_string()];
1638
1639 let result = validate_token2022_extensions(&config);
1640 assert!(result.is_err());
1641 assert!(result.unwrap_err().contains("Invalid mint extension name: 'invalid_extension'"));
1642 }
1643
1644 #[test]
1645 fn test_validate_token2022_extensions_invalid_account_extension() {
1646 let mut config = Token2022Config::default();
1647 config.blocked_account_extensions = vec!["invalid_extension".to_string()];
1648
1649 let result = validate_token2022_extensions(&config);
1650 assert!(result.is_err());
1651 assert!(result
1652 .unwrap_err()
1653 .contains("Invalid account extension name: 'invalid_extension'"));
1654 }
1655
1656 #[test]
1657 fn test_validate_token2022_extensions_empty() {
1658 let config = Token2022Config::default();
1659
1660 let result = validate_token2022_extensions(&config);
1661 assert!(result.is_ok());
1662 }
1663
1664 #[tokio::test]
1665 #[serial]
1666 async fn test_validate_with_result_fee_payer_policy_warnings() {
1667 std::env::set_var("JUPITER_API_KEY", "test-api-key");
1668 let config = Config {
1669 validation: ValidationConfig {
1670 max_allowed_lamports: 1_000_000,
1671 max_signatures: 10,
1672 allowed_programs: vec![
1673 SYSTEM_PROGRAM_ID.to_string(),
1674 SPL_TOKEN_PROGRAM_ID.to_string(),
1675 TOKEN_2022_PROGRAM_ID.to_string(),
1676 ],
1677 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1678 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1679 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1680 ]),
1681 disallowed_accounts: vec![],
1682 price_source: PriceSource::Jupiter,
1683 fee_payer_policy: FeePayerPolicy {
1684 system: SystemInstructionPolicy {
1685 allow_transfer: true,
1686 allow_assign: true,
1687 allow_create_account: true,
1688 allow_allocate: true,
1689 nonce: NonceInstructionPolicy {
1690 allow_initialize: true,
1691 allow_advance: true,
1692 allow_withdraw: true,
1693 allow_authorize: true,
1694 },
1695 },
1696 spl_token: SplTokenInstructionPolicy {
1697 allow_transfer: true,
1698 allow_burn: true,
1699 allow_close_account: true,
1700 allow_approve: true,
1701 allow_revoke: true,
1702 allow_set_authority: true,
1703 allow_mint_to: true,
1704 allow_initialize_mint: true,
1705 allow_initialize_account: true,
1706 allow_initialize_multisig: true,
1707 allow_freeze_account: true,
1708 allow_thaw_account: true,
1709 },
1710 token_2022: Token2022InstructionPolicy {
1711 allow_transfer: true,
1712 allow_burn: true,
1713 allow_close_account: true,
1714 allow_approve: true,
1715 allow_revoke: true,
1716 allow_set_authority: true,
1717 allow_mint_to: true,
1718 allow_initialize_mint: true,
1719 allow_initialize_account: true,
1720 allow_initialize_multisig: true,
1721 allow_freeze_account: true,
1722 allow_thaw_account: true,
1723 },
1724 },
1725 price: PriceConfig { model: PriceModel::Free },
1726 token_2022: Token2022Config::default(),
1727 allow_durable_transactions: false,
1728 max_price_staleness_slots: 0,
1729 },
1730 metrics: MetricsConfig::default(),
1731 kora: KoraConfig::default(),
1732 };
1733
1734 let _ = update_config(config.clone());
1735
1736 let rpc_client = RpcClient::new_with_commitment(
1737 "http://localhost:8899".to_string(),
1738 CommitmentConfig::confirmed(),
1739 );
1740 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1741 assert!(result.is_ok());
1742 let warnings = result.unwrap();
1743
1744 assert!(warnings
1747 .iter()
1748 .any(|w| w.contains("System transfers") && w.contains("allow_transfer")));
1749 assert!(warnings
1750 .iter()
1751 .any(|w| w.contains("System Assign instructions") && w.contains("allow_assign")));
1752 assert!(warnings.iter().any(|w| w.contains("System CreateAccount instructions")
1753 && w.contains("allow_create_account")));
1754 assert!(warnings
1755 .iter()
1756 .any(|w| w.contains("System Allocate instructions") && w.contains("allow_allocate")));
1757
1758 assert!(warnings
1760 .iter()
1761 .any(|w| w.contains("nonce account initialization") && w.contains("allow_initialize")));
1762 assert!(warnings
1763 .iter()
1764 .any(|w| w.contains("nonce account advancement") && w.contains("allow_advance")));
1765 assert!(warnings
1766 .iter()
1767 .any(|w| w.contains("nonce account withdrawals") && w.contains("allow_withdraw")));
1768 assert!(warnings
1769 .iter()
1770 .any(|w| w.contains("nonce authority changes") && w.contains("allow_authorize")));
1771
1772 assert!(warnings
1774 .iter()
1775 .any(|w| w.contains("SPL Token transfers") && w.contains("allow_transfer")));
1776 assert!(warnings
1777 .iter()
1778 .any(|w| w.contains("SPL Token burn operations") && w.contains("allow_burn")));
1779 assert!(warnings
1780 .iter()
1781 .any(|w| w.contains("SPL Token CloseAccount") && w.contains("allow_close_account")));
1782 assert!(warnings
1783 .iter()
1784 .any(|w| w.contains("SPL Token approve") && w.contains("allow_approve")));
1785 assert!(warnings
1786 .iter()
1787 .any(|w| w.contains("SPL Token revoke") && w.contains("allow_revoke")));
1788 assert!(warnings
1789 .iter()
1790 .any(|w| w.contains("SPL Token SetAuthority") && w.contains("allow_set_authority")));
1791 assert!(warnings
1792 .iter()
1793 .any(|w| w.contains("SPL Token MintTo") && w.contains("allow_mint_to")));
1794 assert!(
1795 warnings
1796 .iter()
1797 .any(|w| w.contains("SPL Token InitializeMint")
1798 && w.contains("allow_initialize_mint"))
1799 );
1800 assert!(warnings
1801 .iter()
1802 .any(|w| w.contains("SPL Token InitializeAccount")
1803 && w.contains("allow_initialize_account")));
1804 assert!(warnings.iter().any(|w| w.contains("SPL Token InitializeMultisig")
1805 && w.contains("allow_initialize_multisig")));
1806 assert!(warnings
1807 .iter()
1808 .any(|w| w.contains("SPL Token FreezeAccount") && w.contains("allow_freeze_account")));
1809 assert!(warnings
1810 .iter()
1811 .any(|w| w.contains("SPL Token ThawAccount") && w.contains("allow_thaw_account")));
1812
1813 assert!(warnings
1815 .iter()
1816 .any(|w| w.contains("Token2022 transfers") && w.contains("allow_transfer")));
1817 assert!(warnings
1818 .iter()
1819 .any(|w| w.contains("Token2022 burn operations") && w.contains("allow_burn")));
1820 assert!(warnings
1821 .iter()
1822 .any(|w| w.contains("Token2022 CloseAccount") && w.contains("allow_close_account")));
1823 assert!(warnings
1824 .iter()
1825 .any(|w| w.contains("Token2022 approve") && w.contains("allow_approve")));
1826 assert!(warnings
1827 .iter()
1828 .any(|w| w.contains("Token2022 revoke") && w.contains("allow_revoke")));
1829 assert!(warnings
1830 .iter()
1831 .any(|w| w.contains("Token2022 SetAuthority") && w.contains("allow_set_authority")));
1832 assert!(warnings
1833 .iter()
1834 .any(|w| w.contains("Token2022 MintTo") && w.contains("allow_mint_to")));
1835 assert!(
1836 warnings
1837 .iter()
1838 .any(|w| w.contains("Token2022 InitializeMint")
1839 && w.contains("allow_initialize_mint"))
1840 );
1841 assert!(warnings
1842 .iter()
1843 .any(|w| w.contains("Token2022 InitializeAccount")
1844 && w.contains("allow_initialize_account")));
1845 assert!(warnings.iter().any(|w| w.contains("Token2022 InitializeMultisig")
1846 && w.contains("allow_initialize_multisig")));
1847 assert!(warnings
1848 .iter()
1849 .any(|w| w.contains("Token2022 FreezeAccount") && w.contains("allow_freeze_account")));
1850 assert!(warnings
1851 .iter()
1852 .any(|w| w.contains("Token2022 ThawAccount") && w.contains("allow_thaw_account")));
1853
1854 let fee_payer_warnings: Vec<_> =
1856 warnings.iter().filter(|w| w.contains("Fee payer policy")).collect();
1857 for warning in fee_payer_warnings {
1858 assert!(warning.contains("Risk:"));
1859 assert!(warning.contains("Consider setting"));
1860 }
1861 }
1862
1863 #[tokio::test]
1864 #[serial]
1865 async fn test_check_token_mint_extensions_permanent_delegate() {
1866 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1867
1868 let mint_with_delegate =
1869 create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::PermanentDelegate]);
1870 let mint_pubkey = Pubkey::new_unique();
1871
1872 let rpc_client = create_mock_rpc_client_with_account(&mint_with_delegate);
1873 let mut warnings = Vec::new();
1874
1875 ConfigValidator::check_token_mint_extensions(
1876 &rpc_client,
1877 &[mint_pubkey.to_string()],
1878 &mut warnings,
1879 )
1880 .await;
1881
1882 assert_eq!(warnings.len(), 1);
1883 assert!(warnings[0].contains("PermanentDelegate extension"));
1884 assert!(warnings[0].contains(&mint_pubkey.to_string()));
1885 assert!(warnings[0].contains("Risk:"));
1886 assert!(warnings[0].contains("permanent delegate can transfer or burn tokens"));
1887 }
1888
1889 #[tokio::test]
1890 #[serial]
1891 async fn test_check_token_mint_extensions_transfer_hook() {
1892 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1893
1894 let mint_with_hook =
1895 create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::TransferHook]);
1896 let mint_pubkey = Pubkey::new_unique();
1897
1898 let rpc_client = create_mock_rpc_client_with_account(&mint_with_hook);
1899 let mut warnings = Vec::new();
1900
1901 ConfigValidator::check_token_mint_extensions(
1902 &rpc_client,
1903 &[mint_pubkey.to_string()],
1904 &mut warnings,
1905 )
1906 .await;
1907
1908 assert_eq!(warnings.len(), 1);
1909 assert!(warnings[0].contains("TransferHook extension"));
1910 assert!(warnings[0].contains(&mint_pubkey.to_string()));
1911 assert!(warnings[0].contains("Risk:"));
1912 assert!(warnings[0].contains("custom program executes on every transfer"));
1913 }
1914
1915 #[tokio::test]
1916 #[serial]
1917 async fn test_check_token_mint_extensions_both() {
1918 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1919
1920 let mint_with_both = create_mock_token2022_mint_with_extensions(
1921 6,
1922 vec![ExtensionType::PermanentDelegate, ExtensionType::TransferHook],
1923 );
1924 let mint_pubkey = Pubkey::new_unique();
1925
1926 let rpc_client = create_mock_rpc_client_with_account(&mint_with_both);
1927 let mut warnings = Vec::new();
1928
1929 ConfigValidator::check_token_mint_extensions(
1930 &rpc_client,
1931 &[mint_pubkey.to_string()],
1932 &mut warnings,
1933 )
1934 .await;
1935
1936 assert_eq!(warnings.len(), 2);
1938 assert!(warnings.iter().any(|w| w.contains("PermanentDelegate extension")));
1939 assert!(warnings.iter().any(|w| w.contains("TransferHook extension")));
1940 }
1941
1942 #[tokio::test]
1943 #[serial]
1944 async fn test_check_token_mint_extensions_no_risky_extensions() {
1945 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1946
1947 let mint_with_safe =
1948 create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::MintCloseAuthority]);
1949 let mint_pubkey = Pubkey::new_unique();
1950
1951 let rpc_client = create_mock_rpc_client_with_account(&mint_with_safe);
1952 let mut warnings = Vec::new();
1953
1954 ConfigValidator::check_token_mint_extensions(
1955 &rpc_client,
1956 &[mint_pubkey.to_string()],
1957 &mut warnings,
1958 )
1959 .await;
1960
1961 assert_eq!(warnings.len(), 0);
1962 }
1963
1964 #[tokio::test]
1965 #[serial]
1966 async fn test_durable_transactions_warning_when_enabled() {
1967 std::env::set_var("JUPITER_API_KEY", "test-api-key");
1968 let config = Config {
1969 validation: ValidationConfig {
1970 max_allowed_lamports: 1_000_000,
1971 max_signatures: 10,
1972 allowed_programs: vec![
1973 SYSTEM_PROGRAM_ID.to_string(),
1974 SPL_TOKEN_PROGRAM_ID.to_string(),
1975 ],
1976 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1977 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1978 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1979 ]),
1980 disallowed_accounts: vec![],
1981 price_source: PriceSource::Jupiter,
1982 fee_payer_policy: FeePayerPolicy::default(),
1983 price: PriceConfig::default(),
1984 token_2022: Token2022Config::default(),
1985 allow_durable_transactions: true, max_price_staleness_slots: 0,
1987 },
1988 kora: KoraConfig::default(),
1989 metrics: MetricsConfig::default(),
1990 };
1991
1992 let _ = update_config(config);
1993
1994 let rpc_client = RpcMockBuilder::new().build();
1995 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1996 assert!(result.is_ok());
1997 let warnings = result.unwrap();
1998
1999 assert!(warnings.iter().any(|w| w.contains("allow_durable_transactions is enabled")));
2000 assert!(warnings.iter().any(|w| w.contains("hold signed transactions indefinitely")));
2001 }
2002
2003 #[tokio::test]
2004 #[serial]
2005 async fn test_durable_transactions_no_warning_when_disabled() {
2006 std::env::set_var("JUPITER_API_KEY", "test-api-key");
2007 let config = Config {
2008 validation: ValidationConfig {
2009 max_allowed_lamports: 1_000_000,
2010 max_signatures: 10,
2011 allowed_programs: vec![
2012 SYSTEM_PROGRAM_ID.to_string(),
2013 SPL_TOKEN_PROGRAM_ID.to_string(),
2014 ],
2015 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2016 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2017 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2018 ]),
2019 disallowed_accounts: vec![],
2020 price_source: PriceSource::Jupiter,
2021 fee_payer_policy: FeePayerPolicy::default(),
2022 price: PriceConfig::default(),
2023 token_2022: Token2022Config::default(),
2024 allow_durable_transactions: false, max_price_staleness_slots: 0,
2026 },
2027 kora: KoraConfig::default(),
2028 metrics: MetricsConfig::default(),
2029 };
2030
2031 let _ = update_config(config);
2032
2033 let rpc_client = RpcMockBuilder::new().build();
2034 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2035 assert!(result.is_ok());
2036 let warnings = result.unwrap();
2037
2038 assert!(!warnings.iter().any(|w| w.contains("allow_durable_transactions")));
2039 }
2040
2041 #[tokio::test]
2042 #[serial]
2043 async fn test_jupiter_price_source_requires_api_key() {
2044 std::env::remove_var("JUPITER_API_KEY");
2046
2047 let config = Config {
2048 validation: ValidationConfig {
2049 max_allowed_lamports: 1_000_000,
2050 max_signatures: 10,
2051 allowed_programs: vec![
2052 SYSTEM_PROGRAM_ID.to_string(),
2053 SPL_TOKEN_PROGRAM_ID.to_string(),
2054 ],
2055 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2056 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2057 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2058 ]),
2059 disallowed_accounts: vec![],
2060 price_source: PriceSource::Jupiter, fee_payer_policy: FeePayerPolicy::default(),
2062 price: PriceConfig::default(),
2063 token_2022: Token2022Config::default(),
2064 allow_durable_transactions: false,
2065 max_price_staleness_slots: 0,
2066 },
2067 kora: KoraConfig::default(),
2068 metrics: MetricsConfig::default(),
2069 };
2070
2071 let _ = update_config(config);
2072
2073 let rpc_client = RpcMockBuilder::new().build();
2074 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2075
2076 assert!(result.is_err());
2078 let errors = result.unwrap_err();
2079 assert!(errors.iter().any(|e| e.contains("JUPITER_API_KEY")));
2080 assert!(errors.iter().any(|e| e.contains("price_source = Jupiter")));
2081 }
2082
2083 #[tokio::test]
2084 #[serial]
2085 async fn test_lighthouse_enabled_not_in_allowed_programs_error() {
2086 let config = Config {
2087 validation: ValidationConfig {
2088 max_allowed_lamports: 1_000_000,
2089 max_signatures: 10,
2090 allowed_programs: vec![
2091 SYSTEM_PROGRAM_ID.to_string(),
2092 SPL_TOKEN_PROGRAM_ID.to_string(),
2093 ], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2095 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2096 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2097 ]),
2098 disallowed_accounts: vec![],
2099 price_source: PriceSource::Jupiter,
2100 fee_payer_policy: FeePayerPolicy::default(),
2101 price: PriceConfig::default(),
2102 token_2022: Token2022Config::default(),
2103 allow_durable_transactions: false,
2104 max_price_staleness_slots: 0,
2105 },
2106 kora: KoraConfig {
2107 lighthouse: LighthouseConfig {
2108 enabled: true,
2109 fail_if_transaction_size_overflow: true,
2110 },
2111 ..Default::default()
2112 },
2113 metrics: MetricsConfig::default(),
2114 };
2115
2116 let _ = update_config(config);
2117
2118 let rpc_client = RpcMockBuilder::new().build();
2119 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2120 assert!(result.is_err());
2121 let errors = result.unwrap_err();
2122
2123 assert!(errors
2124 .iter()
2125 .any(|e| e.contains("Lighthouse is enabled but")
2126 && e.contains("is not in allowed_programs")));
2127 }
2128
2129 #[tokio::test]
2130 #[serial]
2131 async fn test_lighthouse_enabled_in_disallowed_accounts_error() {
2132 let config = Config {
2133 validation: ValidationConfig {
2134 max_allowed_lamports: 1_000_000,
2135 max_signatures: 10,
2136 allowed_programs: vec![
2137 SYSTEM_PROGRAM_ID.to_string(),
2138 SPL_TOKEN_PROGRAM_ID.to_string(),
2139 LIGHTHOUSE_PROGRAM_ID.to_string(), ],
2141 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2142 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2143 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2144 ]),
2145 disallowed_accounts: vec![LIGHTHOUSE_PROGRAM_ID.to_string()], price_source: PriceSource::Jupiter,
2147 fee_payer_policy: FeePayerPolicy::default(),
2148 price: PriceConfig::default(),
2149 token_2022: Token2022Config::default(),
2150 allow_durable_transactions: false,
2151 max_price_staleness_slots: 0,
2152 },
2153 kora: KoraConfig {
2154 lighthouse: LighthouseConfig {
2155 enabled: true,
2156 fail_if_transaction_size_overflow: true,
2157 },
2158 ..Default::default()
2159 },
2160 metrics: MetricsConfig::default(),
2161 };
2162
2163 let _ = update_config(config);
2164
2165 let rpc_client = RpcMockBuilder::new().build();
2166 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2167 assert!(result.is_err());
2168 let errors = result.unwrap_err();
2169
2170 assert!(errors
2171 .iter()
2172 .any(|e| e.contains("Lighthouse is enabled but")
2173 && e.contains("is in disallowed_accounts")));
2174 }
2175
2176 #[tokio::test]
2177 #[serial]
2178 async fn test_lighthouse_disabled_no_validation() {
2179 std::env::set_var("JUPITER_API_KEY", "test-api-key");
2180 let config = Config {
2181 validation: ValidationConfig {
2182 max_allowed_lamports: 1_000_000,
2183 max_signatures: 10,
2184 allowed_programs: vec![
2185 SYSTEM_PROGRAM_ID.to_string(),
2186 SPL_TOKEN_PROGRAM_ID.to_string(),
2187 ], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
2189 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
2190 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
2191 ]),
2192 disallowed_accounts: vec![],
2193 price_source: PriceSource::Jupiter,
2194 fee_payer_policy: FeePayerPolicy::default(),
2195 price: PriceConfig::default(),
2196 token_2022: Token2022Config::default(),
2197 allow_durable_transactions: false,
2198 max_price_staleness_slots: 0,
2199 },
2200 kora: KoraConfig {
2201 lighthouse: LighthouseConfig {
2202 enabled: false, fail_if_transaction_size_overflow: true,
2204 },
2205 ..Default::default()
2206 },
2207 metrics: MetricsConfig::default(),
2208 };
2209
2210 let _ = update_config(config);
2211
2212 let rpc_client = RpcMockBuilder::new().build();
2213 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
2214 assert!(result.is_ok());
2215 let warnings = result.unwrap();
2216
2217 assert!(!warnings.iter().any(|w| w.contains("Lighthouse")));
2219 }
2220}