1use std::{path::Path, str::FromStr};
2
3use crate::{
4 admin::token_util::find_missing_atas,
5 config::{FeePayerPolicy, SplTokenConfig, Token2022Config},
6 fee::price::PriceModel,
7 oracle::PriceSource,
8 signer::SignerPoolConfig,
9 state::get_config,
10 token::{spl_token_2022_util, token::TokenUtil},
11 validator::{
12 account_validator::{validate_account, AccountType},
13 cache_validator::CacheValidator,
14 signer_validator::SignerValidator,
15 },
16 KoraError,
17};
18use solana_client::nonblocking::rpc_client::RpcClient;
19use solana_sdk::{account::Account, pubkey::Pubkey};
20use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
21use spl_token_2022_interface::{
22 extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions},
23 state::Mint as Token2022MintState,
24 ID as TOKEN_2022_PROGRAM_ID,
25};
26use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID;
27
28pub struct ConfigValidator {}
29
30impl ConfigValidator {
31 async fn check_token_mint_extensions(
33 rpc_client: &RpcClient,
34 allowed_tokens: &[String],
35 warnings: &mut Vec<String>,
36 ) {
37 for token_str in allowed_tokens {
38 let token_pubkey = match Pubkey::from_str(token_str) {
39 Ok(pk) => pk,
40 Err(_) => continue, };
42
43 let account: Account = match rpc_client.get_account(&token_pubkey).await {
44 Ok(acc) => acc,
45 Err(_) => continue, };
47
48 if account.owner != TOKEN_2022_PROGRAM_ID {
49 continue;
50 }
51
52 let mint_with_extensions =
53 match StateWithExtensions::<Token2022MintState>::unpack(&account.data) {
54 Ok(m) => m,
55 Err(_) => continue, };
57
58 if mint_with_extensions
59 .get_extension::<spl_token_2022_interface::extension::permanent_delegate::PermanentDelegate>()
60 .is_ok()
61 {
62 warnings.push(format!(
63 "⚠️ SECURITY: Token {} has PermanentDelegate extension. \
64 Risk: The permanent delegate can transfer or burn tokens at any time without owner approval. \
65 This creates significant risks for payment tokens as funds can be seized after payment. \
66 Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
67 token_str
68 ));
69 }
70
71 if mint_with_extensions
72 .get_extension::<spl_token_2022_interface::extension::transfer_hook::TransferHook>()
73 .is_ok()
74 {
75 warnings.push(format!(
76 "⚠️ SECURITY: Token {} has TransferHook extension. \
77 Risk: A custom program executes on every transfer which can reject transfers \
78 or introduce external dependencies and attack surface. \
79 Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].",
80 token_str
81 ));
82 }
83 }
84 }
85
86 fn validate_fee_payer_policy(policy: &FeePayerPolicy, warnings: &mut Vec<String>) {
88 macro_rules! check_fee_payer_policy {
89 ($($category:ident, $field:ident, $description:expr, $risk:expr);* $(;)?) => {
90 $(
91 if policy.$category.$field {
92 warnings.push(format!(
93 "⚠️ SECURITY: Fee payer policy allows {} ({}). \
94 Risk: {}. \
95 Consider setting [validation.fee_payer_policy.{}] {}=false to prevent abuse.",
96 $description,
97 stringify!($field),
98 $risk,
99 stringify!($category),
100 stringify!($field)
101 ));
102 }
103 )*
104 };
105 }
106
107 check_fee_payer_policy! {
108 system, allow_transfer, "System transfers",
109 "Users can make the fee payer transfer arbitrary SOL amounts. This can drain your fee payer account";
110
111 system, allow_assign, "System Assign instructions",
112 "Users can make the fee payer reassign ownership of its accounts. This can compromise account control";
113
114 system, allow_create_account, "System CreateAccount instructions",
115 "Users can make the fee payer pay for arbitrary account creations. This can drain your fee payer account";
116
117 system, allow_allocate, "System Allocate instructions",
118 "Users can make the fee payer allocate space for accounts. This can be used to waste resources";
119
120 spl_token, allow_transfer, "SPL Token transfers",
121 "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
122
123 spl_token, allow_burn, "SPL Token burn operations",
124 "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
125
126 spl_token, allow_close_account, "SPL Token CloseAccount instructions",
127 "Users can make the fee payer close token accounts. This can disrupt operations and drain fee payer";
128
129 spl_token, allow_approve, "SPL Token approve operations",
130 "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
131
132 spl_token, allow_revoke, "SPL Token revoke operations",
133 "Users can make the fee payer revoke delegates. This can disrupt authorized operations";
134
135 spl_token, allow_set_authority, "SPL Token SetAuthority instructions",
136 "Users can make the fee payer transfer authority. This can lead to complete loss of control";
137
138 spl_token, allow_mint_to, "SPL Token MintTo operations",
139 "Users can make the fee payer mint tokens. This can inflate token supply";
140
141 spl_token, allow_initialize_mint, "SPL Token InitializeMint instructions",
142 "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
143
144 spl_token, allow_initialize_account, "SPL Token InitializeAccount instructions",
145 "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
146
147 spl_token, allow_initialize_multisig, "SPL Token InitializeMultisig instructions",
148 "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
149
150 spl_token, allow_freeze_account, "SPL Token FreezeAccount instructions",
151 "Users can make the fee payer freeze token accounts. This can disrupt token operations";
152
153 spl_token, allow_thaw_account, "SPL Token ThawAccount instructions",
154 "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
155
156 token_2022, allow_transfer, "Token2022 transfers",
157 "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts";
158
159 token_2022, allow_burn, "Token2022 burn operations",
160 "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets";
161
162 token_2022, allow_close_account, "Token2022 CloseAccount instructions",
163 "Users can make the fee payer close token accounts. This can disrupt operations";
164
165 token_2022, allow_approve, "Token2022 approve operations",
166 "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers";
167
168 token_2022, allow_revoke, "Token2022 revoke operations",
169 "Users can make the fee payer revoke delegates. This can disrupt authorized operations";
170
171 token_2022, allow_set_authority, "Token2022 SetAuthority instructions",
172 "Users can make the fee payer transfer authority. This can lead to complete loss of control";
173
174 token_2022, allow_mint_to, "Token2022 MintTo operations",
175 "Users can make the fee payer mint tokens. This can inflate token supply";
176
177 token_2022, allow_initialize_mint, "Token2022 InitializeMint instructions",
178 "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities";
179
180 token_2022, allow_initialize_account, "Token2022 InitializeAccount instructions",
181 "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer";
182
183 token_2022, allow_initialize_multisig, "Token2022 InitializeMultisig instructions",
184 "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations";
185
186 token_2022, allow_freeze_account, "Token2022 FreezeAccount instructions",
187 "Users can make the fee payer freeze token accounts. This can disrupt token operations";
188
189 token_2022, allow_thaw_account, "Token2022 ThawAccount instructions",
190 "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies";
191 }
192
193 macro_rules! check_nonce_policy {
195 ($($field:ident, $description:expr, $risk:expr);* $(;)?) => {
196 $(
197 if policy.system.nonce.$field {
198 warnings.push(format!(
199 "⚠️ SECURITY: Fee payer policy allows {} (nonce.{}). \
200 Risk: {}. \
201 Consider setting [validation.fee_payer_policy.system.nonce] {}=false to prevent abuse.",
202 $description,
203 stringify!($field),
204 $risk,
205 stringify!($field)
206 ));
207 }
208 )*
209 };
210 }
211
212 check_nonce_policy! {
213 allow_initialize, "nonce account initialization",
214 "Users can make the fee payer the authority of nonce accounts. This can create unexpected control relationships";
215
216 allow_advance, "nonce account advancement",
217 "Users can make the fee payer advance nonce accounts. This can be used to manipulate nonce states";
218
219 allow_withdraw, "nonce account withdrawals",
220 "Users can make the fee payer withdraw from nonce accounts. This can drain nonce account balances";
221
222 allow_authorize, "nonce authority changes",
223 "Users can make the fee payer transfer nonce authority. This can lead to loss of control over nonce accounts";
224 }
225 }
226
227 pub async fn validate(_rpc_client: &RpcClient) -> Result<(), KoraError> {
228 let config = &get_config()?;
229
230 if config.validation.allowed_tokens.is_empty() {
231 return Err(KoraError::InternalServerError("No tokens enabled".to_string()));
232 }
233
234 TokenUtil::check_valid_tokens(&config.validation.allowed_tokens)?;
235
236 if let Some(payment_address) = &config.kora.payment_address {
237 if let Err(e) = Pubkey::from_str(payment_address) {
238 return Err(KoraError::InternalServerError(format!(
239 "Invalid payment address: {e}"
240 )));
241 }
242 }
243
244 Ok(())
245 }
246
247 pub async fn validate_with_result(
248 rpc_client: &RpcClient,
249 skip_rpc_validation: bool,
250 ) -> Result<Vec<String>, Vec<String>> {
251 Self::validate_with_result_and_signers(rpc_client, skip_rpc_validation, None::<&Path>).await
252 }
253}
254
255impl ConfigValidator {
256 pub async fn validate_with_result_and_signers<P: AsRef<Path>>(
257 rpc_client: &RpcClient,
258 skip_rpc_validation: bool,
259 signers_config_path: Option<P>,
260 ) -> Result<Vec<String>, Vec<String>> {
261 let mut errors = Vec::new();
262 let mut warnings = Vec::new();
263
264 let config = match get_config() {
265 Ok(c) => c,
266 Err(e) => {
267 errors.push(format!("Failed to get config: {e}"));
268 return Err(errors);
269 }
270 };
271
272 if config.kora.rate_limit == 0 {
274 warnings.push("Rate limit is set to 0 - this will block all requests".to_string());
275 }
276
277 if let Some(payment_address) = &config.kora.payment_address {
279 if let Err(e) = Pubkey::from_str(payment_address) {
280 errors.push(format!("Invalid payment address: {e}"));
281 }
282 }
283
284 let methods = &config.kora.enabled_methods;
286 if !methods.iter().any(|enabled| enabled) {
287 warnings.push(
288 "All rpc methods are disabled - this will block all functionality".to_string(),
289 );
290 }
291
292 if config.validation.max_allowed_lamports == 0 {
294 warnings
295 .push("Max allowed lamports is 0 - this will block all SOL transfers".to_string());
296 }
297
298 if config.validation.max_signatures == 0 {
300 warnings.push("Max signatures is 0 - this will block all transactions".to_string());
301 }
302
303 if matches!(config.validation.price_source, PriceSource::Mock) {
305 warnings.push("Using Mock price source - not suitable for production".to_string());
306 }
307
308 if config.validation.allow_durable_transactions {
309 warnings.push(
310 "⚠️ SECURITY: allow_durable_transactions is enabled. \
311 Risk: Users can hold signed transactions indefinitely and execute them much later. \
312 Token values may change, your fee payer may run low on funds, or you may no longer \
313 want to subsidize these transactions. \
314 Consider disabling durable transactions unless specifically required."
315 .to_string(),
316 );
317 }
318
319 if config.validation.allowed_programs.is_empty() {
321 warnings.push(
322 "No allowed programs configured - this will block all transactions".to_string(),
323 );
324 } else {
325 if !config.validation.allowed_programs.contains(&SYSTEM_PROGRAM_ID.to_string()) {
326 warnings.push("Missing System Program in allowed programs - SOL transfers and account operations will be blocked".to_string());
327 }
328 if !config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string())
329 && !config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string())
330 {
331 warnings.push("Missing Token Program in allowed programs - SPL token operations will be blocked".to_string());
332 }
333 }
334
335 if config.validation.allowed_tokens.is_empty() {
337 errors.push("No allowed tokens configured".to_string());
338 } else if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.allowed_tokens) {
339 errors.push(format!("Invalid token address: {e}"));
340 }
341
342 if let Err(e) =
344 TokenUtil::check_valid_tokens(config.validation.allowed_spl_paid_tokens.as_slice())
345 {
346 errors.push(format!("Invalid spl paid token address: {e}"));
347 }
348
349 if matches!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All) {
351 warnings.push(
352 "⚠️ Using 'All' for allowed_spl_paid_tokens - this accepts ANY SPL token for payment. \
353 Consider using an explicit allowlist to reduce volatility risk and protect against \
354 potentially malicious or worthless tokens being used for fees.".to_string()
355 );
356 }
357
358 if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
360 errors.push(format!("Invalid disallowed account address: {e}"));
361 }
362
363 if let Err(e) = validate_token2022_extensions(&config.validation.token_2022) {
365 errors.push(format!("Token2022 extension validation failed: {e}"));
366 }
367
368 if !config.validation.token_2022.is_mint_extension_blocked(ExtensionType::PermanentDelegate)
370 {
371 warnings.push(
372 "⚠️ SECURITY: PermanentDelegate extension is NOT blocked. Tokens with this extension \
373 allow the delegate to transfer/burn tokens at any time without owner approval. \
374 This creates significant risks:\n\
375 - Payment tokens: Funds can be seized after payment\n\
376 Consider adding \"permanent_delegate\" to blocked_mint_extensions in [validation.token2022] \
377 unless explicitly needed for your use case.".to_string()
378 );
379 }
380
381 let fees_enabled = !matches!(config.validation.price.model, PriceModel::Free);
383
384 if fees_enabled {
385 let has_token_program =
387 config.validation.allowed_programs.contains(&SPL_TOKEN_PROGRAM_ID.to_string());
388 let has_token22_program =
389 config.validation.allowed_programs.contains(&TOKEN_2022_PROGRAM_ID.to_string());
390
391 if !has_token_program && !has_token22_program {
392 errors.push("When fees are enabled, at least one token program (SPL Token or Token2022) must be in allowed_programs".to_string());
393 }
394
395 if !config.validation.allowed_spl_paid_tokens.has_tokens() {
397 errors.push(
398 "When fees are enabled, allowed_spl_paid_tokens cannot be empty".to_string(),
399 );
400 }
401 } else {
402 warnings.push(
403 "⚠️ SECURITY: Free pricing model enabled - all transactions will be processed \
404 without charging fees."
405 .to_string(),
406 );
407 }
408
409 for paid_token in &config.validation.allowed_spl_paid_tokens {
411 if !config.validation.allowed_tokens.contains(paid_token) {
412 errors.push(format!(
413 "Token {paid_token} in allowed_spl_paid_tokens must also be in allowed_tokens"
414 ));
415 }
416 }
417
418 Self::validate_fee_payer_policy(&config.validation.fee_payer_policy, &mut warnings);
420
421 match &config.validation.price.model {
423 PriceModel::Fixed { amount, token, strict } => {
424 if *amount == 0 {
425 warnings
426 .push("Fixed price amount is 0 - transactions will be free".to_string());
427 }
428 if Pubkey::from_str(token).is_err() {
429 errors.push(format!("Invalid token address for fixed price: {token}"));
430 }
431 if !config.validation.supports_token(token) {
432 errors.push(format!(
433 "Token address for fixed price is not in allowed spl paid tokens: {token}"
434 ));
435 }
436
437 let has_auth =
439 config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
440 if !has_auth {
441 warnings.push(
442 "⚠️ SECURITY: Fixed pricing with NO authentication enabled. \
443 Without authentication, anyone can spam transactions at your expense. \
444 Consider enabling api_key or hmac_secret in [kora.auth]."
445 .to_string(),
446 );
447 }
448
449 if *strict {
451 warnings.push(
452 "Strict pricing mode enabled. \
453 Transactions where fee payer outflow exceeds the fixed price will be rejected."
454 .to_string(),
455 );
456 }
457 }
458 PriceModel::Margin { margin } => {
459 if *margin < 0.0 {
460 errors.push("Margin cannot be negative".to_string());
461 } else if *margin > 1.0 {
462 warnings.push(format!("Margin is {}% - this is very high", margin * 100.0));
463 }
464 }
465 _ => {}
466 };
467
468 let has_auth = config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some();
470 if !has_auth {
471 warnings.push(
472 "⚠️ SECURITY: No authentication configured (neither api_key nor hmac_secret). \
473 Authentication is strongly recommended for production deployments. \
474 Consider enabling api_key or hmac_secret in [kora.auth]."
475 .to_string(),
476 );
477 }
478
479 let usage_config = &config.kora.usage_limit;
481 if usage_config.enabled {
482 let (usage_errors, usage_warnings) = CacheValidator::validate(usage_config).await;
483 errors.extend(usage_errors);
484 warnings.extend(usage_warnings);
485 }
486
487 if !skip_rpc_validation {
489 for program_str in &config.validation.allowed_programs {
491 if let Ok(program_pubkey) = Pubkey::from_str(program_str) {
492 if let Err(e) = validate_account(
493 config,
494 rpc_client,
495 &program_pubkey,
496 Some(AccountType::Program),
497 )
498 .await
499 {
500 errors.push(format!("Program {program_str} validation failed: {e}"));
501 }
502 }
503 }
504
505 for token_str in &config.validation.allowed_tokens {
507 if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
508 if let Err(e) =
509 validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
510 .await
511 {
512 errors.push(format!("Token {token_str} validation failed: {e}"));
513 }
514 }
515 }
516
517 for token_str in &config.validation.allowed_spl_paid_tokens {
519 if let Ok(token_pubkey) = Pubkey::from_str(token_str) {
520 if let Err(e) =
521 validate_account(config, rpc_client, &token_pubkey, Some(AccountType::Mint))
522 .await
523 {
524 errors.push(format!("SPL paid token {token_str} validation failed: {e}"));
525 }
526 }
527 }
528
529 Self::check_token_mint_extensions(
531 rpc_client,
532 &config.validation.allowed_tokens,
533 &mut warnings,
534 )
535 .await;
536
537 if let Some(payment_address) = &config.kora.payment_address {
539 if let Ok(payment_address) = Pubkey::from_str(payment_address) {
540 match find_missing_atas(config, rpc_client, &payment_address).await {
541 Ok(atas_to_create) => {
542 if !atas_to_create.is_empty() {
543 errors.push(format!(
544 "Missing ATAs for payment address: {payment_address}"
545 ));
546 }
547 }
548 Err(e) => errors.push(format!("Failed to find missing ATAs: {e}")),
549 }
550 } else {
551 errors.push(format!("Invalid payment address: {payment_address}"));
552 }
553 }
554 }
555
556 if let Some(path) = signers_config_path {
558 match SignerPoolConfig::load_config(path.as_ref()) {
559 Ok(signer_config) => {
560 let (signer_warnings, signer_errors) =
561 SignerValidator::validate_with_result(&signer_config);
562 warnings.extend(signer_warnings);
563 errors.extend(signer_errors);
564 }
565 Err(e) => {
566 errors.push(format!("Failed to load signers config: {e}"));
567 }
568 }
569 } else {
570 println!("ℹ️ Signers configuration not validated. Include --signers-config path/to/signers.toml to validate signers");
571 }
572
573 println!("=== Configuration Validation ===");
575 if errors.is_empty() {
576 println!("✓ Configuration validation successful!");
577 } else {
578 println!("✗ Configuration validation failed!");
579 println!("\n❌ Errors:");
580 for error in &errors {
581 println!(" - {error}");
582 }
583 println!("\nPlease fix the configuration errors above before deploying.");
584 }
585
586 if !warnings.is_empty() {
587 println!("\n⚠️ Warnings:");
588 for warning in &warnings {
589 println!(" - {warning}");
590 }
591 }
592
593 if errors.is_empty() {
594 Ok(warnings)
595 } else {
596 Err(errors)
597 }
598 }
599}
600
601fn validate_token2022_extensions(config: &Token2022Config) -> Result<(), String> {
603 for ext_name in &config.blocked_mint_extensions {
605 if spl_token_2022_util::parse_mint_extension_string(ext_name).is_none() {
606 return Err(format!(
607 "Invalid mint extension name: '{ext_name}'. Valid names are: {:?}",
608 spl_token_2022_util::get_all_mint_extension_names()
609 ));
610 }
611 }
612
613 for ext_name in &config.blocked_account_extensions {
615 if spl_token_2022_util::parse_account_extension_string(ext_name).is_none() {
616 return Err(format!(
617 "Invalid account extension name: '{ext_name}'. Valid names are: {:?}",
618 spl_token_2022_util::get_all_account_extension_names()
619 ));
620 }
621 }
622
623 Ok(())
624}
625
626#[cfg(test)]
627mod tests {
628 use crate::{
629 config::{
630 AuthConfig, BundleConfig, CacheConfig, Config, EnabledMethods, FeePayerPolicy,
631 KoraConfig, MetricsConfig, NonceInstructionPolicy, SplTokenConfig,
632 SplTokenInstructionPolicy, SystemInstructionPolicy, Token2022InstructionPolicy,
633 UsageLimitConfig, ValidationConfig,
634 },
635 constant::DEFAULT_MAX_REQUEST_BODY_SIZE,
636 fee::price::PriceConfig,
637 state::update_config,
638 tests::{
639 account_mock::create_mock_token2022_mint_with_extensions,
640 common::{
641 create_mock_non_executable_account, create_mock_program_account,
642 create_mock_rpc_client_account_not_found, create_mock_rpc_client_with_account,
643 create_mock_rpc_client_with_mint, RpcMockBuilder,
644 },
645 config_mock::ConfigMockBuilder,
646 },
647 };
648 use serial_test::serial;
649 use solana_commitment_config::CommitmentConfig;
650 use spl_token_2022_interface::extension::ExtensionType;
651
652 use super::*;
653
654 #[tokio::test]
655 #[serial]
656 async fn test_validate_config() {
657 let mut config = Config {
658 validation: ValidationConfig {
659 max_allowed_lamports: 1000000000,
660 max_signatures: 10,
661 allowed_programs: vec!["program1".to_string()],
662 allowed_tokens: vec!["token1".to_string()],
663 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec!["token3".to_string()]),
664 disallowed_accounts: vec!["account1".to_string()],
665 price_source: PriceSource::Jupiter,
666 fee_payer_policy: FeePayerPolicy::default(),
667 price: PriceConfig::default(),
668 token_2022: Token2022Config::default(),
669 allow_durable_transactions: false,
670 },
671 kora: KoraConfig::default(),
672 metrics: MetricsConfig::default(),
673 };
674
675 let _ = update_config(config.clone());
677
678 config.validation.allowed_tokens = vec![];
680 let _ = update_config(config);
681
682 let rpc_client = RpcClient::new_with_commitment(
683 "http://localhost:8899".to_string(),
684 CommitmentConfig::confirmed(),
685 );
686 let result = ConfigValidator::validate(&rpc_client).await;
687 assert!(result.is_err());
688 assert!(matches!(result.unwrap_err(), KoraError::InternalServerError(_)));
689 }
690
691 #[tokio::test]
692 #[serial]
693 async fn test_validate_with_result_successful_config() {
694 let config = Config {
695 validation: ValidationConfig {
696 max_allowed_lamports: 1_000_000,
697 max_signatures: 10,
698 allowed_programs: vec![
699 SYSTEM_PROGRAM_ID.to_string(),
700 SPL_TOKEN_PROGRAM_ID.to_string(),
701 ],
702 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
703 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
704 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
705 ]),
706 disallowed_accounts: vec![],
707 price_source: PriceSource::Jupiter,
708 fee_payer_policy: FeePayerPolicy::default(),
709 price: PriceConfig::default(),
710 token_2022: Token2022Config::default(),
711 allow_durable_transactions: false,
712 },
713 kora: KoraConfig::default(),
714 metrics: MetricsConfig::default(),
715 };
716
717 let _ = update_config(config);
719
720 let rpc_client = RpcClient::new_with_commitment(
721 "http://localhost:8899".to_string(),
722 CommitmentConfig::confirmed(),
723 );
724 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
725 assert!(result.is_ok());
726 let warnings = result.unwrap();
727 assert_eq!(warnings.len(), 2);
729 assert!(warnings.iter().any(|w| w.contains("PermanentDelegate")));
730 assert!(warnings.iter().any(|w| w.contains("No authentication configured")));
731 }
732
733 #[tokio::test]
734 #[serial]
735 async fn test_validate_with_result_warnings() {
736 let config = Config {
737 validation: ValidationConfig {
738 max_allowed_lamports: 0, max_signatures: 0, allowed_programs: vec![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
742 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
743 disallowed_accounts: vec![],
744 price_source: PriceSource::Mock, fee_payer_policy: FeePayerPolicy::default(),
746 price: PriceConfig { model: PriceModel::Free },
747 token_2022: Token2022Config::default(),
748 allow_durable_transactions: false,
749 },
750 kora: KoraConfig {
751 rate_limit: 0, max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
753 enabled_methods: EnabledMethods {
754 liveness: false,
755 estimate_transaction_fee: false,
756 get_supported_tokens: false,
757 sign_transaction: false,
758 sign_and_send_transaction: false,
759 transfer_transaction: false,
760 get_blockhash: false,
761 get_config: false,
762 get_payer_signer: false,
763 get_version: false,
764 estimate_bundle_fee: false,
765 sign_and_send_bundle: false,
766 sign_bundle: false,
767 },
768 auth: AuthConfig::default(),
769 payment_address: None,
770 cache: CacheConfig::default(),
771 usage_limit: UsageLimitConfig::default(),
772 bundle: BundleConfig::default(),
773 },
774 metrics: MetricsConfig::default(),
775 };
776
777 let _ = update_config(config);
779
780 let rpc_client = RpcClient::new_with_commitment(
781 "http://localhost:8899".to_string(),
782 CommitmentConfig::confirmed(),
783 );
784 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
785 assert!(result.is_ok());
786 let warnings = result.unwrap();
787
788 assert!(!warnings.is_empty());
789 assert!(warnings.iter().any(|w| w.contains("Rate limit is set to 0")));
790 assert!(warnings.iter().any(|w| w.contains("All rpc methods are disabled")));
791 assert!(warnings.iter().any(|w| w.contains("Max allowed lamports is 0")));
792 assert!(warnings.iter().any(|w| w.contains("Max signatures is 0")));
793 assert!(warnings.iter().any(|w| w.contains("Using Mock price source")));
794 assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
795 }
796
797 #[tokio::test]
798 #[serial]
799 async fn test_validate_with_result_missing_system_program_warning() {
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 let config = Config {
967 validation: ValidationConfig {
968 max_allowed_lamports: 1_000_000,
969 max_signatures: 10,
970 allowed_programs: vec![
971 SYSTEM_PROGRAM_ID.to_string(),
972 SPL_TOKEN_PROGRAM_ID.to_string(),
973 ],
974 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
975 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
976 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
977 ]),
978 disallowed_accounts: vec![],
979 price_source: PriceSource::Jupiter,
980 fee_payer_policy: FeePayerPolicy::default(),
981 price: PriceConfig {
982 model: PriceModel::Fixed {
983 amount: 0, token: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
985 strict: false,
986 },
987 },
988 token_2022: Token2022Config::default(),
989 allow_durable_transactions: false,
990 },
991 metrics: MetricsConfig::default(),
992 kora: KoraConfig::default(),
993 };
994
995 let _ = update_config(config);
996
997 let rpc_client = RpcClient::new_with_commitment(
998 "http://localhost:8899".to_string(),
999 CommitmentConfig::confirmed(),
1000 );
1001 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1002 assert!(result.is_ok());
1003 let warnings = result.unwrap();
1004
1005 assert!(warnings
1006 .iter()
1007 .any(|w| w.contains("Fixed price amount is 0 - transactions will be free")));
1008 }
1009
1010 #[tokio::test]
1011 #[serial]
1012 async fn test_validate_with_result_fee_validation_errors() {
1013 let config = Config {
1014 validation: ValidationConfig {
1015 max_allowed_lamports: 1_000_000,
1016 max_signatures: 10,
1017 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1019 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), disallowed_accounts: vec![],
1021 price_source: PriceSource::Jupiter,
1022 fee_payer_policy: FeePayerPolicy::default(),
1023 price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1024 token_2022: Token2022Config::default(),
1025 allow_durable_transactions: false,
1026 },
1027 metrics: MetricsConfig::default(),
1028 kora: KoraConfig::default(),
1029 };
1030
1031 let _ = update_config(config);
1032
1033 let rpc_client = RpcClient::new_with_commitment(
1034 "http://localhost:8899".to_string(),
1035 CommitmentConfig::confirmed(),
1036 );
1037 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1038 assert!(result.is_err());
1039 let errors = result.unwrap_err();
1040
1041 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")));
1042 assert!(errors
1043 .iter()
1044 .any(|e| e.contains("When fees are enabled, allowed_spl_paid_tokens cannot be empty")));
1045 }
1046
1047 #[tokio::test]
1048 #[serial]
1049 async fn test_validate_with_result_fee_and_any_spl_token_allowed() {
1050 let config = Config {
1051 validation: ValidationConfig {
1052 max_allowed_lamports: 1_000_000,
1053 max_signatures: 10,
1054 allowed_programs: vec![
1055 SYSTEM_PROGRAM_ID.to_string(),
1056 SPL_TOKEN_PROGRAM_ID.to_string(),
1057 ],
1058 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1059 allowed_spl_paid_tokens: SplTokenConfig::All, disallowed_accounts: vec![],
1061 price_source: PriceSource::Jupiter,
1062 fee_payer_policy: FeePayerPolicy::default(),
1063 price: PriceConfig { model: PriceModel::Margin { margin: 0.1 } },
1064 token_2022: Token2022Config::default(),
1065 allow_durable_transactions: false,
1066 },
1067 metrics: MetricsConfig::default(),
1068 kora: KoraConfig::default(),
1069 };
1070
1071 let _ = update_config(config);
1072
1073 let rpc_client = RpcMockBuilder::new().build();
1074
1075 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1076 assert!(result.is_ok());
1077
1078 let warnings = result.unwrap();
1080 assert!(warnings.iter().any(|w| w.contains("Using 'All' for allowed_spl_paid_tokens")));
1081 assert!(warnings.iter().any(|w| w.contains("volatility risk")));
1082 }
1083
1084 #[tokio::test]
1085 #[serial]
1086 async fn test_validate_with_result_paid_tokens_not_in_allowed_tokens() {
1087 let config = Config {
1088 validation: ValidationConfig {
1089 max_allowed_lamports: 1_000_000,
1090 max_signatures: 10,
1091 allowed_programs: vec![
1092 SYSTEM_PROGRAM_ID.to_string(),
1093 SPL_TOKEN_PROGRAM_ID.to_string(),
1094 ],
1095 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1096 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1097 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), ]),
1099 disallowed_accounts: vec![],
1100 price_source: PriceSource::Jupiter,
1101 fee_payer_policy: FeePayerPolicy::default(),
1102 price: PriceConfig { model: PriceModel::Free },
1103 token_2022: Token2022Config::default(),
1104 allow_durable_transactions: false,
1105 },
1106 metrics: MetricsConfig::default(),
1107 kora: KoraConfig::default(),
1108 };
1109
1110 let _ = update_config(config);
1111
1112 let rpc_client = RpcMockBuilder::new().build();
1113 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1114 assert!(result.is_err());
1115 let errors = result.unwrap_err();
1116
1117 assert!(errors.iter().any(|e| e.contains("Token EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v in allowed_spl_paid_tokens must also be in allowed_tokens")));
1118 }
1119
1120 fn create_program_only_config() -> Config {
1122 Config {
1123 validation: ValidationConfig {
1124 max_allowed_lamports: 1_000_000,
1125 max_signatures: 10,
1126 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1127 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()], allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1129 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1130 ]),
1131 disallowed_accounts: vec![],
1132 price_source: PriceSource::Jupiter,
1133 fee_payer_policy: FeePayerPolicy::default(),
1134 price: PriceConfig { model: PriceModel::Free },
1135 token_2022: Token2022Config::default(),
1136 allow_durable_transactions: false,
1137 },
1138 metrics: MetricsConfig::default(),
1139 kora: KoraConfig::default(),
1140 }
1141 }
1142
1143 fn create_token_only_config() -> Config {
1145 Config {
1146 validation: ValidationConfig {
1147 max_allowed_lamports: 1_000_000,
1148 max_signatures: 10,
1149 allowed_programs: vec![], allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1151 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]), disallowed_accounts: vec![],
1153 price_source: PriceSource::Jupiter,
1154 fee_payer_policy: FeePayerPolicy::default(),
1155 price: PriceConfig { model: PriceModel::Free },
1156 token_2022: Token2022Config::default(),
1157 allow_durable_transactions: false,
1158 },
1159 metrics: MetricsConfig::default(),
1160 kora: KoraConfig::default(),
1161 }
1162 }
1163
1164 #[tokio::test]
1165 #[serial]
1166 async fn test_validate_with_result_rpc_validation_valid_program() {
1167 let config = create_program_only_config();
1168
1169 let _ = update_config(config);
1171
1172 let rpc_client = create_mock_rpc_client_with_account(&create_mock_program_account());
1173
1174 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1177 assert!(result.is_err());
1178 let errors = result.unwrap_err();
1179 assert!(errors.iter().any(|e| e.contains("Token")
1181 && e.contains("validation failed")
1182 && e.contains("not found")));
1183 assert!(!errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1184 }
1185
1186 #[tokio::test]
1187 #[serial]
1188 async fn test_validate_with_result_rpc_validation_valid_token_mint() {
1189 let config = create_token_only_config();
1190
1191 let _ = update_config(config);
1193
1194 let rpc_client = create_mock_rpc_client_with_mint(6);
1195
1196 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1199 assert!(result.is_ok());
1200 let warnings = result.unwrap();
1202 assert!(warnings.iter().any(|w| w.contains("No allowed programs configured")));
1203 }
1204
1205 #[tokio::test]
1206 #[serial]
1207 async fn test_validate_with_result_rpc_validation_non_executable_program_fails() {
1208 let config = Config {
1209 validation: ValidationConfig {
1210 max_allowed_lamports: 1_000_000,
1211 max_signatures: 10,
1212 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1213 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1214 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1215 disallowed_accounts: vec![],
1216 price_source: PriceSource::Jupiter,
1217 fee_payer_policy: FeePayerPolicy::default(),
1218 price: PriceConfig { model: PriceModel::Free },
1219 token_2022: Token2022Config::default(),
1220 allow_durable_transactions: false,
1221 },
1222 metrics: MetricsConfig::default(),
1223 kora: KoraConfig::default(),
1224 };
1225
1226 let _ = update_config(config);
1228
1229 let rpc_client = create_mock_rpc_client_with_account(&create_mock_non_executable_account());
1230
1231 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1233 assert!(result.is_err());
1234 let errors = result.unwrap_err();
1235 assert!(errors.iter().any(|e| e.contains("Program") && e.contains("validation failed")));
1236 }
1237
1238 #[tokio::test]
1239 #[serial]
1240 async fn test_validate_with_result_rpc_validation_account_not_found_fails() {
1241 let config = Config {
1242 validation: ValidationConfig {
1243 max_allowed_lamports: 1_000_000,
1244 max_signatures: 10,
1245 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1246 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1247 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1248 disallowed_accounts: vec![],
1249 price_source: PriceSource::Jupiter,
1250 fee_payer_policy: FeePayerPolicy::default(),
1251 price: PriceConfig { model: PriceModel::Free },
1252 token_2022: Token2022Config::default(),
1253 allow_durable_transactions: false,
1254 },
1255 metrics: MetricsConfig::default(),
1256 kora: KoraConfig::default(),
1257 };
1258
1259 let _ = update_config(config);
1260
1261 let rpc_client = create_mock_rpc_client_account_not_found();
1262
1263 let result = ConfigValidator::validate_with_result(&rpc_client, false).await;
1265 assert!(result.is_err());
1266 let errors = result.unwrap_err();
1267 assert!(errors.len() >= 2, "Should have validation errors for programs and tokens");
1268 }
1269
1270 #[tokio::test]
1271 #[serial]
1272 async fn test_validate_with_result_skip_rpc_validation() {
1273 let config = Config {
1274 validation: ValidationConfig {
1275 max_allowed_lamports: 1_000_000,
1276 max_signatures: 10,
1277 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1278 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1279 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1280 disallowed_accounts: vec![],
1281 price_source: PriceSource::Jupiter,
1282 fee_payer_policy: FeePayerPolicy::default(),
1283 price: PriceConfig { model: PriceModel::Free },
1284 token_2022: Token2022Config::default(),
1285 allow_durable_transactions: false,
1286 },
1287 metrics: MetricsConfig::default(),
1288 kora: KoraConfig::default(),
1289 };
1290
1291 let _ = update_config(config);
1292
1293 let rpc_client = create_mock_rpc_client_account_not_found();
1295
1296 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1298 assert!(result.is_ok()); }
1300
1301 #[tokio::test]
1302 #[serial]
1303 async fn test_validate_with_result_valid_token2022_extensions() {
1304 let config = Config {
1305 validation: ValidationConfig {
1306 max_allowed_lamports: 1_000_000,
1307 max_signatures: 10,
1308 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1309 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1310 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1311 disallowed_accounts: vec![],
1312 price_source: PriceSource::Jupiter,
1313 fee_payer_policy: FeePayerPolicy::default(),
1314 price: PriceConfig { model: PriceModel::Free },
1315 token_2022: {
1316 let mut config = Token2022Config::default();
1317 config.blocked_mint_extensions =
1318 vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1319 config.blocked_account_extensions =
1320 vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1321 config
1322 },
1323 allow_durable_transactions: false,
1324 },
1325 metrics: MetricsConfig::default(),
1326 kora: KoraConfig::default(),
1327 };
1328
1329 let _ = update_config(config);
1330
1331 let rpc_client = RpcClient::new_with_commitment(
1332 "http://localhost:8899".to_string(),
1333 CommitmentConfig::confirmed(),
1334 );
1335 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1336 assert!(result.is_ok());
1337 }
1338
1339 #[tokio::test]
1340 #[serial]
1341 async fn test_validate_with_result_invalid_token2022_mint_extension() {
1342 let config = Config {
1343 validation: ValidationConfig {
1344 max_allowed_lamports: 1_000_000,
1345 max_signatures: 10,
1346 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1347 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1348 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1349 disallowed_accounts: vec![],
1350 price_source: PriceSource::Jupiter,
1351 fee_payer_policy: FeePayerPolicy::default(),
1352 price: PriceConfig { model: PriceModel::Free },
1353 token_2022: {
1354 let mut config = Token2022Config::default();
1355 config.blocked_mint_extensions = vec!["invalid_mint_extension".to_string()];
1356 config
1357 },
1358 allow_durable_transactions: false,
1359 },
1360 metrics: MetricsConfig::default(),
1361 kora: KoraConfig::default(),
1362 };
1363
1364 let _ = update_config(config);
1365
1366 let rpc_client = RpcClient::new_with_commitment(
1367 "http://localhost:8899".to_string(),
1368 CommitmentConfig::confirmed(),
1369 );
1370 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1371 assert!(result.is_err());
1372 let errors = result.unwrap_err();
1373 assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1374 && e.contains("Invalid mint extension name: 'invalid_mint_extension'")));
1375 }
1376
1377 #[tokio::test]
1378 #[serial]
1379 async fn test_validate_with_result_invalid_token2022_account_extension() {
1380 let config = Config {
1381 validation: ValidationConfig {
1382 max_allowed_lamports: 1_000_000,
1383 max_signatures: 10,
1384 allowed_programs: vec![SYSTEM_PROGRAM_ID.to_string()],
1385 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1386 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![]),
1387 disallowed_accounts: vec![],
1388 price_source: PriceSource::Jupiter,
1389 fee_payer_policy: FeePayerPolicy::default(),
1390 price: PriceConfig { model: PriceModel::Free },
1391 token_2022: {
1392 let mut config = Token2022Config::default();
1393 config.blocked_account_extensions =
1394 vec!["invalid_account_extension".to_string()];
1395 config
1396 },
1397 allow_durable_transactions: false,
1398 },
1399 metrics: MetricsConfig::default(),
1400 kora: KoraConfig::default(),
1401 };
1402
1403 let _ = update_config(config);
1404
1405 let rpc_client = RpcClient::new_with_commitment(
1406 "http://localhost:8899".to_string(),
1407 CommitmentConfig::confirmed(),
1408 );
1409 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1410 assert!(result.is_err());
1411 let errors = result.unwrap_err();
1412 assert!(errors.iter().any(|e| e.contains("Token2022 extension validation failed")
1413 && e.contains("Invalid account extension name: 'invalid_account_extension'")));
1414 }
1415
1416 #[test]
1417 fn test_validate_token2022_extensions_valid() {
1418 let mut config = Token2022Config::default();
1419 config.blocked_mint_extensions =
1420 vec!["transfer_fee_config".to_string(), "pausable".to_string()];
1421 config.blocked_account_extensions =
1422 vec!["memo_transfer".to_string(), "cpi_guard".to_string()];
1423
1424 let result = validate_token2022_extensions(&config);
1425 assert!(result.is_ok());
1426 }
1427
1428 #[test]
1429 fn test_validate_token2022_extensions_invalid_mint_extension() {
1430 let mut config = Token2022Config::default();
1431 config.blocked_mint_extensions = vec!["invalid_extension".to_string()];
1432
1433 let result = validate_token2022_extensions(&config);
1434 assert!(result.is_err());
1435 assert!(result.unwrap_err().contains("Invalid mint extension name: 'invalid_extension'"));
1436 }
1437
1438 #[test]
1439 fn test_validate_token2022_extensions_invalid_account_extension() {
1440 let mut config = Token2022Config::default();
1441 config.blocked_account_extensions = vec!["invalid_extension".to_string()];
1442
1443 let result = validate_token2022_extensions(&config);
1444 assert!(result.is_err());
1445 assert!(result
1446 .unwrap_err()
1447 .contains("Invalid account extension name: 'invalid_extension'"));
1448 }
1449
1450 #[test]
1451 fn test_validate_token2022_extensions_empty() {
1452 let config = Token2022Config::default();
1453
1454 let result = validate_token2022_extensions(&config);
1455 assert!(result.is_ok());
1456 }
1457
1458 #[tokio::test]
1459 #[serial]
1460 async fn test_validate_with_result_fee_payer_policy_warnings() {
1461 let config = Config {
1462 validation: ValidationConfig {
1463 max_allowed_lamports: 1_000_000,
1464 max_signatures: 10,
1465 allowed_programs: vec![
1466 SYSTEM_PROGRAM_ID.to_string(),
1467 SPL_TOKEN_PROGRAM_ID.to_string(),
1468 TOKEN_2022_PROGRAM_ID.to_string(),
1469 ],
1470 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1471 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1472 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1473 ]),
1474 disallowed_accounts: vec![],
1475 price_source: PriceSource::Jupiter,
1476 fee_payer_policy: FeePayerPolicy {
1477 system: SystemInstructionPolicy {
1478 allow_transfer: true,
1479 allow_assign: true,
1480 allow_create_account: true,
1481 allow_allocate: true,
1482 nonce: NonceInstructionPolicy {
1483 allow_initialize: true,
1484 allow_advance: true,
1485 allow_withdraw: true,
1486 allow_authorize: true,
1487 },
1488 },
1489 spl_token: SplTokenInstructionPolicy {
1490 allow_transfer: true,
1491 allow_burn: true,
1492 allow_close_account: true,
1493 allow_approve: true,
1494 allow_revoke: true,
1495 allow_set_authority: true,
1496 allow_mint_to: true,
1497 allow_initialize_mint: true,
1498 allow_initialize_account: true,
1499 allow_initialize_multisig: true,
1500 allow_freeze_account: true,
1501 allow_thaw_account: true,
1502 },
1503 token_2022: Token2022InstructionPolicy {
1504 allow_transfer: true,
1505 allow_burn: true,
1506 allow_close_account: true,
1507 allow_approve: true,
1508 allow_revoke: true,
1509 allow_set_authority: true,
1510 allow_mint_to: true,
1511 allow_initialize_mint: true,
1512 allow_initialize_account: true,
1513 allow_initialize_multisig: true,
1514 allow_freeze_account: true,
1515 allow_thaw_account: true,
1516 },
1517 },
1518 price: PriceConfig { model: PriceModel::Free },
1519 token_2022: Token2022Config::default(),
1520 allow_durable_transactions: false,
1521 },
1522 metrics: MetricsConfig::default(),
1523 kora: KoraConfig::default(),
1524 };
1525
1526 let _ = update_config(config.clone());
1527
1528 let rpc_client = RpcClient::new_with_commitment(
1529 "http://localhost:8899".to_string(),
1530 CommitmentConfig::confirmed(),
1531 );
1532 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1533 assert!(result.is_ok());
1534 let warnings = result.unwrap();
1535
1536 assert!(warnings
1539 .iter()
1540 .any(|w| w.contains("System transfers") && w.contains("allow_transfer")));
1541 assert!(warnings
1542 .iter()
1543 .any(|w| w.contains("System Assign instructions") && w.contains("allow_assign")));
1544 assert!(warnings.iter().any(|w| w.contains("System CreateAccount instructions")
1545 && w.contains("allow_create_account")));
1546 assert!(warnings
1547 .iter()
1548 .any(|w| w.contains("System Allocate instructions") && w.contains("allow_allocate")));
1549
1550 assert!(warnings
1552 .iter()
1553 .any(|w| w.contains("nonce account initialization") && w.contains("allow_initialize")));
1554 assert!(warnings
1555 .iter()
1556 .any(|w| w.contains("nonce account advancement") && w.contains("allow_advance")));
1557 assert!(warnings
1558 .iter()
1559 .any(|w| w.contains("nonce account withdrawals") && w.contains("allow_withdraw")));
1560 assert!(warnings
1561 .iter()
1562 .any(|w| w.contains("nonce authority changes") && w.contains("allow_authorize")));
1563
1564 assert!(warnings
1566 .iter()
1567 .any(|w| w.contains("SPL Token transfers") && w.contains("allow_transfer")));
1568 assert!(warnings
1569 .iter()
1570 .any(|w| w.contains("SPL Token burn operations") && w.contains("allow_burn")));
1571 assert!(warnings
1572 .iter()
1573 .any(|w| w.contains("SPL Token CloseAccount") && w.contains("allow_close_account")));
1574 assert!(warnings
1575 .iter()
1576 .any(|w| w.contains("SPL Token approve") && w.contains("allow_approve")));
1577 assert!(warnings
1578 .iter()
1579 .any(|w| w.contains("SPL Token revoke") && w.contains("allow_revoke")));
1580 assert!(warnings
1581 .iter()
1582 .any(|w| w.contains("SPL Token SetAuthority") && w.contains("allow_set_authority")));
1583 assert!(warnings
1584 .iter()
1585 .any(|w| w.contains("SPL Token MintTo") && w.contains("allow_mint_to")));
1586 assert!(
1587 warnings
1588 .iter()
1589 .any(|w| w.contains("SPL Token InitializeMint")
1590 && w.contains("allow_initialize_mint"))
1591 );
1592 assert!(warnings
1593 .iter()
1594 .any(|w| w.contains("SPL Token InitializeAccount")
1595 && w.contains("allow_initialize_account")));
1596 assert!(warnings.iter().any(|w| w.contains("SPL Token InitializeMultisig")
1597 && w.contains("allow_initialize_multisig")));
1598 assert!(warnings
1599 .iter()
1600 .any(|w| w.contains("SPL Token FreezeAccount") && w.contains("allow_freeze_account")));
1601 assert!(warnings
1602 .iter()
1603 .any(|w| w.contains("SPL Token ThawAccount") && w.contains("allow_thaw_account")));
1604
1605 assert!(warnings
1607 .iter()
1608 .any(|w| w.contains("Token2022 transfers") && w.contains("allow_transfer")));
1609 assert!(warnings
1610 .iter()
1611 .any(|w| w.contains("Token2022 burn operations") && w.contains("allow_burn")));
1612 assert!(warnings
1613 .iter()
1614 .any(|w| w.contains("Token2022 CloseAccount") && w.contains("allow_close_account")));
1615 assert!(warnings
1616 .iter()
1617 .any(|w| w.contains("Token2022 approve") && w.contains("allow_approve")));
1618 assert!(warnings
1619 .iter()
1620 .any(|w| w.contains("Token2022 revoke") && w.contains("allow_revoke")));
1621 assert!(warnings
1622 .iter()
1623 .any(|w| w.contains("Token2022 SetAuthority") && w.contains("allow_set_authority")));
1624 assert!(warnings
1625 .iter()
1626 .any(|w| w.contains("Token2022 MintTo") && w.contains("allow_mint_to")));
1627 assert!(
1628 warnings
1629 .iter()
1630 .any(|w| w.contains("Token2022 InitializeMint")
1631 && w.contains("allow_initialize_mint"))
1632 );
1633 assert!(warnings
1634 .iter()
1635 .any(|w| w.contains("Token2022 InitializeAccount")
1636 && w.contains("allow_initialize_account")));
1637 assert!(warnings.iter().any(|w| w.contains("Token2022 InitializeMultisig")
1638 && w.contains("allow_initialize_multisig")));
1639 assert!(warnings
1640 .iter()
1641 .any(|w| w.contains("Token2022 FreezeAccount") && w.contains("allow_freeze_account")));
1642 assert!(warnings
1643 .iter()
1644 .any(|w| w.contains("Token2022 ThawAccount") && w.contains("allow_thaw_account")));
1645
1646 let fee_payer_warnings: Vec<_> =
1648 warnings.iter().filter(|w| w.contains("Fee payer policy")).collect();
1649 for warning in fee_payer_warnings {
1650 assert!(warning.contains("Risk:"));
1651 assert!(warning.contains("Consider setting"));
1652 }
1653 }
1654
1655 #[tokio::test]
1656 #[serial]
1657 async fn test_check_token_mint_extensions_permanent_delegate() {
1658 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1659
1660 let mint_with_delegate =
1661 create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::PermanentDelegate]);
1662 let mint_pubkey = Pubkey::new_unique();
1663
1664 let rpc_client = create_mock_rpc_client_with_account(&mint_with_delegate);
1665 let mut warnings = Vec::new();
1666
1667 ConfigValidator::check_token_mint_extensions(
1668 &rpc_client,
1669 &[mint_pubkey.to_string()],
1670 &mut warnings,
1671 )
1672 .await;
1673
1674 assert_eq!(warnings.len(), 1);
1675 assert!(warnings[0].contains("PermanentDelegate extension"));
1676 assert!(warnings[0].contains(&mint_pubkey.to_string()));
1677 assert!(warnings[0].contains("Risk:"));
1678 assert!(warnings[0].contains("permanent delegate can transfer or burn tokens"));
1679 }
1680
1681 #[tokio::test]
1682 #[serial]
1683 async fn test_check_token_mint_extensions_transfer_hook() {
1684 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1685
1686 let mint_with_hook =
1687 create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::TransferHook]);
1688 let mint_pubkey = Pubkey::new_unique();
1689
1690 let rpc_client = create_mock_rpc_client_with_account(&mint_with_hook);
1691 let mut warnings = Vec::new();
1692
1693 ConfigValidator::check_token_mint_extensions(
1694 &rpc_client,
1695 &[mint_pubkey.to_string()],
1696 &mut warnings,
1697 )
1698 .await;
1699
1700 assert_eq!(warnings.len(), 1);
1701 assert!(warnings[0].contains("TransferHook extension"));
1702 assert!(warnings[0].contains(&mint_pubkey.to_string()));
1703 assert!(warnings[0].contains("Risk:"));
1704 assert!(warnings[0].contains("custom program executes on every transfer"));
1705 }
1706
1707 #[tokio::test]
1708 #[serial]
1709 async fn test_check_token_mint_extensions_both() {
1710 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1711
1712 let mint_with_both = create_mock_token2022_mint_with_extensions(
1713 6,
1714 vec![ExtensionType::PermanentDelegate, ExtensionType::TransferHook],
1715 );
1716 let mint_pubkey = Pubkey::new_unique();
1717
1718 let rpc_client = create_mock_rpc_client_with_account(&mint_with_both);
1719 let mut warnings = Vec::new();
1720
1721 ConfigValidator::check_token_mint_extensions(
1722 &rpc_client,
1723 &[mint_pubkey.to_string()],
1724 &mut warnings,
1725 )
1726 .await;
1727
1728 assert_eq!(warnings.len(), 2);
1730 assert!(warnings.iter().any(|w| w.contains("PermanentDelegate extension")));
1731 assert!(warnings.iter().any(|w| w.contains("TransferHook extension")));
1732 }
1733
1734 #[tokio::test]
1735 #[serial]
1736 async fn test_check_token_mint_extensions_no_risky_extensions() {
1737 let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup();
1738
1739 let mint_with_safe =
1740 create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::MintCloseAuthority]);
1741 let mint_pubkey = Pubkey::new_unique();
1742
1743 let rpc_client = create_mock_rpc_client_with_account(&mint_with_safe);
1744 let mut warnings = Vec::new();
1745
1746 ConfigValidator::check_token_mint_extensions(
1747 &rpc_client,
1748 &[mint_pubkey.to_string()],
1749 &mut warnings,
1750 )
1751 .await;
1752
1753 assert_eq!(warnings.len(), 0);
1754 }
1755
1756 #[tokio::test]
1757 #[serial]
1758 async fn test_durable_transactions_warning_when_enabled() {
1759 let config = Config {
1760 validation: ValidationConfig {
1761 max_allowed_lamports: 1_000_000,
1762 max_signatures: 10,
1763 allowed_programs: vec![
1764 SYSTEM_PROGRAM_ID.to_string(),
1765 SPL_TOKEN_PROGRAM_ID.to_string(),
1766 ],
1767 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1768 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1769 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1770 ]),
1771 disallowed_accounts: vec![],
1772 price_source: PriceSource::Jupiter,
1773 fee_payer_policy: FeePayerPolicy::default(),
1774 price: PriceConfig::default(),
1775 token_2022: Token2022Config::default(),
1776 allow_durable_transactions: true, },
1778 kora: KoraConfig::default(),
1779 metrics: MetricsConfig::default(),
1780 };
1781
1782 let _ = update_config(config);
1783
1784 let rpc_client = RpcMockBuilder::new().build();
1785 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1786 assert!(result.is_ok());
1787 let warnings = result.unwrap();
1788
1789 assert!(warnings.iter().any(|w| w.contains("allow_durable_transactions is enabled")));
1790 assert!(warnings.iter().any(|w| w.contains("hold signed transactions indefinitely")));
1791 }
1792
1793 #[tokio::test]
1794 #[serial]
1795 async fn test_durable_transactions_no_warning_when_disabled() {
1796 let config = Config {
1797 validation: ValidationConfig {
1798 max_allowed_lamports: 1_000_000,
1799 max_signatures: 10,
1800 allowed_programs: vec![
1801 SYSTEM_PROGRAM_ID.to_string(),
1802 SPL_TOKEN_PROGRAM_ID.to_string(),
1803 ],
1804 allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()],
1805 allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![
1806 "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(),
1807 ]),
1808 disallowed_accounts: vec![],
1809 price_source: PriceSource::Jupiter,
1810 fee_payer_policy: FeePayerPolicy::default(),
1811 price: PriceConfig::default(),
1812 token_2022: Token2022Config::default(),
1813 allow_durable_transactions: false, },
1815 kora: KoraConfig::default(),
1816 metrics: MetricsConfig::default(),
1817 };
1818
1819 let _ = update_config(config);
1820
1821 let rpc_client = RpcMockBuilder::new().build();
1822 let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
1823 assert!(result.is_ok());
1824 let warnings = result.unwrap();
1825
1826 assert!(!warnings.iter().any(|w| w.contains("allow_durable_transactions")));
1827 }
1828}