1use serde::{Deserialize, Serialize};
2use solana_sdk::pubkey::Pubkey;
3use spl_token_2022_interface::extension::ExtensionType;
4use std::{fs, path::Path, str::FromStr};
5use toml;
6use utoipa::ToSchema;
7
8use crate::{
9 bundle::JitoConfig,
10 constant::{
11 DEFAULT_CACHE_ACCOUNT_TTL, DEFAULT_CACHE_DEFAULT_TTL,
12 DEFAULT_FEE_PAYER_BALANCE_METRICS_EXPIRY_SECONDS, DEFAULT_MAX_REQUEST_BODY_SIZE,
13 DEFAULT_MAX_TIMESTAMP_AGE, DEFAULT_METRICS_ENDPOINT, DEFAULT_METRICS_PORT,
14 DEFAULT_METRICS_SCRAPE_INTERVAL, DEFAULT_PROTECTED_METHODS,
15 DEFAULT_RECAPTCHA_SCORE_THRESHOLD,
16 },
17 error::KoraError,
18 fee::price::{PriceConfig, PriceModel},
19 oracle::PriceSource,
20 sanitize_error,
21};
22
23pub use crate::usage_limit::{UsageLimitConfig, UsageLimitRuleConfig};
25
26#[derive(Clone, Deserialize)]
27pub struct Config {
28 pub validation: ValidationConfig,
29 pub kora: KoraConfig,
30 #[serde(default)]
31 pub metrics: MetricsConfig,
32}
33
34#[derive(Clone, Serialize, Deserialize, ToSchema)]
35#[serde(default)]
36pub struct MetricsConfig {
37 pub enabled: bool,
38 pub endpoint: String,
39 pub port: u16,
40 pub scrape_interval: u64,
41 pub fee_payer_balance: FeePayerBalanceMetricsConfig,
42}
43
44impl Default for MetricsConfig {
45 fn default() -> Self {
46 Self {
47 enabled: false,
48 endpoint: DEFAULT_METRICS_ENDPOINT.to_string(),
49 port: DEFAULT_METRICS_PORT,
50 scrape_interval: DEFAULT_METRICS_SCRAPE_INTERVAL,
51 fee_payer_balance: FeePayerBalanceMetricsConfig::default(),
52 }
53 }
54}
55
56#[derive(Clone, Serialize, Deserialize, ToSchema)]
57pub struct FeePayerBalanceMetricsConfig {
58 pub enabled: bool,
59 pub expiry_seconds: u64,
60}
61
62impl Default for FeePayerBalanceMetricsConfig {
63 fn default() -> Self {
64 Self { enabled: false, expiry_seconds: DEFAULT_FEE_PAYER_BALANCE_METRICS_EXPIRY_SECONDS }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub enum SplTokenConfig {
70 All,
71 #[serde(untagged)]
72 Allowlist(Vec<String>),
73}
74
75impl Default for SplTokenConfig {
76 fn default() -> Self {
77 SplTokenConfig::Allowlist(vec![])
78 }
79}
80
81impl<'a> IntoIterator for &'a SplTokenConfig {
82 type Item = &'a String;
83 type IntoIter = std::slice::Iter<'a, String>;
84
85 fn into_iter(self) -> Self::IntoIter {
86 match self {
87 SplTokenConfig::All => [].iter(),
88 SplTokenConfig::Allowlist(tokens) => tokens.iter(),
89 }
90 }
91}
92
93impl SplTokenConfig {
94 pub fn has_token(&self, token: &str) -> bool {
95 match self {
96 SplTokenConfig::All => true,
97 SplTokenConfig::Allowlist(tokens) => tokens.iter().any(|s| s == token),
98 }
99 }
100
101 pub fn has_tokens(&self) -> bool {
102 match self {
103 SplTokenConfig::All => true,
104 SplTokenConfig::Allowlist(tokens) => !tokens.is_empty(),
105 }
106 }
107
108 pub fn as_slice(&self) -> &[String] {
109 match self {
110 SplTokenConfig::All => &[],
111 SplTokenConfig::Allowlist(v) => v.as_slice(),
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
117pub struct ValidationConfig {
118 pub max_allowed_lamports: u64,
119 pub max_signatures: u64,
120 pub allowed_programs: Vec<String>,
121 pub allowed_tokens: Vec<String>,
122 pub allowed_spl_paid_tokens: SplTokenConfig,
123 pub disallowed_accounts: Vec<String>,
124 pub price_source: PriceSource,
125 #[serde(default)] pub fee_payer_policy: FeePayerPolicy,
127 #[serde(default)]
128 pub price: PriceConfig,
129 #[serde(default)]
130 pub token_2022: Token2022Config,
131 #[serde(default)]
135 pub allow_durable_transactions: bool,
136}
137
138impl ValidationConfig {
139 pub fn is_payment_required(&self) -> bool {
140 !matches!(&self.price.model, PriceModel::Free)
141 }
142
143 pub fn supports_token(&self, token: &str) -> bool {
144 self.allowed_spl_paid_tokens.has_token(token)
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
149#[serde(default)]
150pub struct FeePayerPolicy {
151 pub system: SystemInstructionPolicy,
152 pub spl_token: SplTokenInstructionPolicy,
153 pub token_2022: Token2022InstructionPolicy,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
157#[serde(default)]
158pub struct SystemInstructionPolicy {
159 pub allow_transfer: bool,
161 pub allow_assign: bool,
163 pub allow_create_account: bool,
165 pub allow_allocate: bool,
167 pub nonce: NonceInstructionPolicy,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
172pub struct NonceInstructionPolicy {
173 pub allow_initialize: bool,
175 pub allow_advance: bool,
177 pub allow_withdraw: bool,
179 pub allow_authorize: bool,
181 }
183
184#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
185pub struct SplTokenInstructionPolicy {
186 pub allow_transfer: bool,
188 pub allow_burn: bool,
190 pub allow_close_account: bool,
192 pub allow_approve: bool,
194 pub allow_revoke: bool,
196 pub allow_set_authority: bool,
198 pub allow_mint_to: bool,
200 pub allow_initialize_mint: bool,
202 pub allow_initialize_account: bool,
204 pub allow_initialize_multisig: bool,
206 pub allow_freeze_account: bool,
208 pub allow_thaw_account: bool,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
213pub struct Token2022InstructionPolicy {
214 pub allow_transfer: bool,
216 pub allow_burn: bool,
218 pub allow_close_account: bool,
220 pub allow_approve: bool,
222 pub allow_revoke: bool,
224 pub allow_set_authority: bool,
226 pub allow_mint_to: bool,
228 pub allow_initialize_mint: bool,
230 pub allow_initialize_account: bool,
232 pub allow_initialize_multisig: bool,
234 pub allow_freeze_account: bool,
236 pub allow_thaw_account: bool,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
241pub struct Token2022Config {
242 pub blocked_mint_extensions: Vec<String>,
243 pub blocked_account_extensions: Vec<String>,
244 #[serde(skip)]
245 parsed_blocked_mint_extensions: Option<Vec<ExtensionType>>,
246 #[serde(skip)]
247 parsed_blocked_account_extensions: Option<Vec<ExtensionType>>,
248}
249
250impl Default for Token2022Config {
251 fn default() -> Self {
252 Self {
253 blocked_mint_extensions: Vec::new(),
254 blocked_account_extensions: Vec::new(),
255 parsed_blocked_mint_extensions: Some(Vec::new()),
256 parsed_blocked_account_extensions: Some(Vec::new()),
257 }
258 }
259}
260
261impl Token2022Config {
262 pub fn initialize(&mut self) -> Result<(), String> {
265 let mut mint_extensions = Vec::new();
266 for name in &self.blocked_mint_extensions {
267 match crate::token::spl_token_2022_util::parse_mint_extension_string(name) {
268 Some(ext) => {
269 mint_extensions.push(ext);
270 }
271 None => {
272 return Err(format!(
273 "Invalid mint extension name: '{}'. Valid names are: {:?}",
274 name,
275 crate::token::spl_token_2022_util::get_all_mint_extension_names()
276 ));
277 }
278 }
279 }
280 self.parsed_blocked_mint_extensions = Some(mint_extensions);
281
282 let mut account_extensions = Vec::new();
283 for name in &self.blocked_account_extensions {
284 match crate::token::spl_token_2022_util::parse_account_extension_string(name) {
285 Some(ext) => {
286 account_extensions.push(ext);
287 }
288 None => {
289 return Err(format!(
290 "Invalid account extension name: '{}'. Valid names are: {:?}",
291 name,
292 crate::token::spl_token_2022_util::get_all_account_extension_names()
293 ));
294 }
295 }
296 }
297 self.parsed_blocked_account_extensions = Some(account_extensions);
298
299 Ok(())
300 }
301
302 pub fn get_blocked_mint_extensions(&self) -> &[ExtensionType] {
304 self.parsed_blocked_mint_extensions.as_deref().unwrap_or(&[])
305 }
306
307 pub fn get_blocked_account_extensions(&self) -> &[ExtensionType] {
309 self.parsed_blocked_account_extensions.as_deref().unwrap_or(&[])
310 }
311
312 pub fn is_mint_extension_blocked(&self, ext: ExtensionType) -> bool {
314 self.get_blocked_mint_extensions().contains(&ext)
315 }
316
317 pub fn is_account_extension_blocked(&self, ext: ExtensionType) -> bool {
319 self.get_blocked_account_extensions().contains(&ext)
320 }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
324#[serde(default)]
325pub struct EnabledMethods {
326 pub liveness: bool,
327 pub estimate_transaction_fee: bool,
328 pub get_supported_tokens: bool,
329 pub get_payer_signer: bool,
330 pub sign_transaction: bool,
331 pub sign_and_send_transaction: bool,
332 pub transfer_transaction: bool,
333 pub get_blockhash: bool,
334 pub get_config: bool,
335 pub get_version: bool,
336 pub estimate_bundle_fee: bool,
338 pub sign_and_send_bundle: bool,
339 pub sign_bundle: bool,
340}
341
342impl EnabledMethods {
343 pub fn iter(&self) -> impl Iterator<Item = bool> {
344 [
345 self.liveness,
346 self.estimate_transaction_fee,
347 self.get_supported_tokens,
348 self.get_payer_signer,
349 self.sign_transaction,
350 self.sign_and_send_transaction,
351 self.transfer_transaction,
352 self.get_blockhash,
353 self.get_config,
354 self.get_version,
355 self.estimate_bundle_fee,
356 self.sign_and_send_bundle,
357 self.sign_bundle,
358 ]
359 .into_iter()
360 }
361
362 pub fn get_enabled_method_names(&self) -> Vec<String> {
364 let mut methods = Vec::new();
365 if self.liveness {
366 methods.push("liveness".to_string());
367 }
368 if self.estimate_transaction_fee {
369 methods.push("estimateTransactionFee".to_string());
370 }
371 if self.estimate_bundle_fee {
372 methods.push("estimateBundleFee".to_string());
373 }
374 if self.get_supported_tokens {
375 methods.push("getSupportedTokens".to_string());
376 }
377 if self.get_payer_signer {
378 methods.push("getPayerSigner".to_string());
379 }
380 if self.sign_transaction {
381 methods.push("signTransaction".to_string());
382 }
383 if self.sign_and_send_transaction {
384 methods.push("signAndSendTransaction".to_string());
385 }
386 if self.transfer_transaction {
387 methods.push("transferTransaction".to_string());
388 }
389 if self.get_blockhash {
390 methods.push("getBlockhash".to_string());
391 }
392 if self.get_config {
393 methods.push("getConfig".to_string());
394 }
395 if self.get_version {
396 methods.push("getVersion".to_string());
397 }
398 if self.sign_and_send_bundle {
399 methods.push("signAndSendBundle".to_string());
400 }
401 if self.sign_bundle {
402 methods.push("signBundle".to_string());
403 }
404 methods
405 }
406}
407
408impl IntoIterator for &EnabledMethods {
409 type Item = bool;
410 type IntoIter = std::array::IntoIter<bool, 13>;
411
412 fn into_iter(self) -> Self::IntoIter {
413 [
414 self.liveness,
415 self.estimate_transaction_fee,
416 self.get_supported_tokens,
417 self.get_payer_signer,
418 self.sign_transaction,
419 self.sign_and_send_transaction,
420 self.transfer_transaction,
421 self.get_blockhash,
422 self.get_config,
423 self.get_version,
424 self.estimate_bundle_fee,
425 self.sign_and_send_bundle,
426 self.sign_bundle,
427 ]
428 .into_iter()
429 }
430}
431
432impl Default for EnabledMethods {
433 fn default() -> Self {
434 Self {
435 liveness: true,
436 estimate_transaction_fee: true,
437 get_supported_tokens: true,
438 get_payer_signer: true,
439 sign_transaction: true,
440 sign_and_send_transaction: true,
441 transfer_transaction: true,
442 get_blockhash: true,
443 get_config: true,
444 get_version: true,
445 estimate_bundle_fee: false,
447 sign_and_send_bundle: false,
448 sign_bundle: false,
449 }
450 }
451}
452
453#[derive(Clone, Serialize, Deserialize, ToSchema)]
454pub struct CacheConfig {
455 pub url: Option<String>,
457 pub enabled: bool,
459 pub default_ttl: u64,
461 pub account_ttl: u64,
463}
464
465impl Default for CacheConfig {
466 fn default() -> Self {
467 Self {
468 url: None,
469 enabled: false,
470 default_ttl: DEFAULT_CACHE_DEFAULT_TTL,
471 account_ttl: DEFAULT_CACHE_ACCOUNT_TTL,
472 }
473 }
474}
475
476#[derive(Clone, Serialize, Deserialize, ToSchema)]
477#[serde(default)]
478pub struct KoraConfig {
479 pub rate_limit: u64,
480 pub max_request_body_size: usize,
481 pub enabled_methods: EnabledMethods,
482 pub auth: AuthConfig,
483 pub payment_address: Option<String>,
485 pub cache: CacheConfig,
486 pub usage_limit: UsageLimitConfig,
487 pub bundle: BundleConfig,
489 pub lighthouse: LighthouseConfig,
491}
492
493impl Default for KoraConfig {
494 fn default() -> Self {
495 Self {
496 rate_limit: 100,
497 max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
498 enabled_methods: EnabledMethods::default(),
499 auth: AuthConfig::default(),
500 payment_address: None,
501 cache: CacheConfig::default(),
502 usage_limit: UsageLimitConfig::default(),
503 bundle: BundleConfig::default(),
504 lighthouse: LighthouseConfig::default(),
505 }
506 }
507}
508
509#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
511#[serde(default)]
512pub struct BundleConfig {
513 pub enabled: bool,
515 pub jito: JitoConfig,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
521#[serde(default)]
522pub struct LighthouseConfig {
523 pub enabled: bool,
525 pub fail_if_transaction_size_overflow: bool,
528}
529
530impl Default for LighthouseConfig {
531 fn default() -> Self {
532 Self { enabled: false, fail_if_transaction_size_overflow: true }
533 }
534}
535
536#[derive(Clone, Serialize, Deserialize, ToSchema)]
537#[serde(default)]
538pub struct AuthConfig {
539 pub api_key: Option<String>,
540 pub hmac_secret: Option<String>,
541 pub recaptcha_secret: Option<String>,
542 pub recaptcha_score_threshold: f64,
543 pub max_timestamp_age: i64,
544 pub protected_methods: Vec<String>,
545}
546
547impl Default for AuthConfig {
548 fn default() -> Self {
549 Self {
550 api_key: None,
551 hmac_secret: None,
552 recaptcha_secret: None,
553 recaptcha_score_threshold: DEFAULT_RECAPTCHA_SCORE_THRESHOLD,
554 max_timestamp_age: DEFAULT_MAX_TIMESTAMP_AGE,
555 protected_methods: DEFAULT_PROTECTED_METHODS.iter().map(|s| s.to_string()).collect(),
556 }
557 }
558}
559
560impl Config {
561 pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config, KoraError> {
562 let contents = fs::read_to_string(path).map_err(|e| {
563 KoraError::InternalServerError(format!(
564 "Failed to read config file: {}",
565 sanitize_error!(e)
566 ))
567 })?;
568
569 let mut config: Config = toml::from_str(&contents).map_err(|e| {
570 KoraError::InternalServerError(format!(
571 "Failed to parse config file: {}",
572 sanitize_error!(e)
573 ))
574 })?;
575
576 config.validation.token_2022.initialize().map_err(|e| {
578 KoraError::InternalServerError(format!(
579 "Failed to initialize Token2022 config: {}",
580 sanitize_error!(e)
581 ))
582 })?;
583
584 Ok(config)
585 }
586}
587
588impl KoraConfig {
589 pub fn get_payment_address(&self, signer_pubkey: &Pubkey) -> Result<Pubkey, KoraError> {
591 if let Some(payment_address_str) = &self.payment_address {
592 let payment_address = Pubkey::from_str(payment_address_str).map_err(|_| {
593 KoraError::InternalServerError("Invalid payment_address format".to_string())
594 })?;
595 Ok(payment_address)
596 } else {
597 Ok(*signer_pubkey)
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use crate::{
605 fee::price::PriceModel,
606 tests::toml_mock::{create_invalid_config, ConfigBuilder},
607 };
608
609 use super::*;
610
611 #[test]
612 fn test_load_valid_config() {
613 let config = ConfigBuilder::new()
614 .with_programs(vec!["program1", "program2"])
615 .with_tokens(vec!["token1", "token2"])
616 .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
617 .with_disallowed_accounts(vec!["account1"])
618 .build_config()
619 .unwrap();
620
621 assert_eq!(config.validation.max_allowed_lamports, 1000000000);
622 assert_eq!(config.validation.max_signatures, 10);
623 assert_eq!(config.validation.allowed_programs, vec!["program1", "program2"]);
624 assert_eq!(config.validation.allowed_tokens, vec!["token1", "token2"]);
625 assert_eq!(
626 config.validation.allowed_spl_paid_tokens,
627 SplTokenConfig::Allowlist(vec!["token3".to_string()])
628 );
629 assert_eq!(config.validation.disallowed_accounts, vec!["account1"]);
630 assert_eq!(config.validation.price_source, PriceSource::Jupiter);
631 assert_eq!(config.kora.rate_limit, 100);
632 assert!(config.kora.enabled_methods.estimate_transaction_fee);
633 assert!(config.kora.enabled_methods.sign_and_send_transaction);
634 }
635
636 #[test]
637 fn test_load_config_with_enabled_methods() {
638 let config = ConfigBuilder::new()
639 .with_programs(vec!["program1", "program2"])
640 .with_tokens(vec!["token1", "token2"])
641 .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
642 .with_disallowed_accounts(vec!["account1"])
643 .with_enabled_methods(&[
644 ("liveness", true),
645 ("estimate_transaction_fee", false),
646 ("get_supported_tokens", true),
647 ("sign_transaction", true),
648 ("sign_and_send_transaction", false),
649 ("transfer_transaction", true),
650 ("get_blockhash", true),
651 ("get_config", true),
652 ("get_payer_signer", true),
653 ("get_version", true),
654 ])
655 .build_config()
656 .unwrap();
657
658 assert_eq!(config.kora.rate_limit, 100);
659 assert!(config.kora.enabled_methods.liveness);
660 assert!(!config.kora.enabled_methods.estimate_transaction_fee);
661 assert!(config.kora.enabled_methods.get_supported_tokens);
662 assert!(config.kora.enabled_methods.sign_transaction);
663 assert!(!config.kora.enabled_methods.sign_and_send_transaction);
664 assert!(config.kora.enabled_methods.transfer_transaction);
665 assert!(config.kora.enabled_methods.get_blockhash);
666 assert!(config.kora.enabled_methods.get_config);
667 }
668
669 #[test]
670 fn test_load_invalid_config() {
671 let result = create_invalid_config("invalid toml content");
672 assert!(result.is_err());
673 }
674
675 #[test]
676 fn test_load_nonexistent_file() {
677 let result = Config::load_config("nonexistent_file.toml");
678 assert!(result.is_err());
679 }
680
681 #[test]
682 fn test_parse_spl_payment_config() {
683 let config =
684 ConfigBuilder::new().with_spl_paid_tokens(SplTokenConfig::All).build_config().unwrap();
685
686 assert_eq!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All);
687 }
688
689 #[test]
690 fn test_parse_margin_price_config() {
691 let config = ConfigBuilder::new().with_margin_price(0.1).build_config().unwrap();
692
693 match &config.validation.price.model {
694 PriceModel::Margin { margin } => {
695 assert_eq!(*margin, 0.1);
696 }
697 _ => panic!("Expected Margin price model"),
698 }
699 }
700
701 #[test]
702 fn test_parse_fixed_price_config() {
703 let config = ConfigBuilder::new()
704 .with_fixed_price(1000000, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")
705 .build_config()
706 .unwrap();
707
708 match &config.validation.price.model {
709 PriceModel::Fixed { amount, token, strict } => {
710 assert_eq!(*amount, 1000000);
711 assert_eq!(token, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
712 assert!(!strict);
713 }
714 _ => panic!("Expected Fixed price model"),
715 }
716 }
717
718 #[test]
719 fn test_parse_free_price_config() {
720 let config = ConfigBuilder::new().with_free_price().build_config().unwrap();
721
722 match &config.validation.price.model {
723 PriceModel::Free => {
724 }
726 _ => panic!("Expected Free price model"),
727 }
728 }
729
730 #[test]
731 fn test_parse_missing_price_config() {
732 let config = ConfigBuilder::new().build_config().unwrap();
733
734 match &config.validation.price.model {
736 PriceModel::Margin { margin } => {
737 assert_eq!(*margin, 0.0);
738 }
739 _ => panic!("Expected default Margin price model with 0.0 margin"),
740 }
741 }
742
743 #[test]
744 fn test_parse_invalid_price_config() {
745 let result = ConfigBuilder::new().with_invalid_price("invalid_type").build_config();
746
747 assert!(result.is_err());
748 if let Err(KoraError::InternalServerError(msg)) = result {
749 assert!(msg.contains("Failed to parse config file"));
750 } else {
751 panic!("Expected InternalServerError with parsing failure message");
752 }
753 }
754
755 #[test]
756 fn test_token2022_config_parsing() {
757 let config = ConfigBuilder::new()
758 .with_token2022_extensions(
759 vec!["transfer_fee_config", "pausable"],
760 vec!["memo_transfer", "cpi_guard"],
761 )
762 .build_config()
763 .unwrap();
764
765 assert_eq!(
766 config.validation.token_2022.blocked_mint_extensions,
767 vec!["transfer_fee_config", "pausable"]
768 );
769 assert_eq!(
770 config.validation.token_2022.blocked_account_extensions,
771 vec!["memo_transfer", "cpi_guard"]
772 );
773
774 let mint_extensions = config.validation.token_2022.get_blocked_mint_extensions();
775 assert_eq!(mint_extensions.len(), 2);
776
777 let account_extensions = config.validation.token_2022.get_blocked_account_extensions();
778 assert_eq!(account_extensions.len(), 2);
779 }
780
781 #[test]
782 fn test_token2022_config_invalid_extension() {
783 let result = ConfigBuilder::new()
784 .with_token2022_extensions(vec!["invalid_extension"], vec![])
785 .build_config();
786
787 assert!(result.is_err());
788 if let Err(KoraError::InternalServerError(msg)) = result {
789 assert!(msg.contains("Failed to initialize Token2022 config"));
790 assert!(msg.contains("Invalid mint extension name: 'invalid_extension'"));
791 } else {
792 panic!("Expected InternalServerError with Token2022 initialization failure");
793 }
794 }
795
796 #[test]
797 fn test_token2022_config_default() {
798 let config = ConfigBuilder::new().build_config().unwrap();
799
800 assert!(config.validation.token_2022.blocked_mint_extensions.is_empty());
801 assert!(config.validation.token_2022.blocked_account_extensions.is_empty());
802
803 assert!(config.validation.token_2022.get_blocked_mint_extensions().is_empty());
804 assert!(config.validation.token_2022.get_blocked_account_extensions().is_empty());
805 }
806
807 #[test]
808 fn test_token2022_extension_blocking_check() {
809 let config = ConfigBuilder::new()
810 .with_token2022_extensions(
811 vec!["transfer_fee_config", "pausable"],
812 vec!["memo_transfer"],
813 )
814 .build_config()
815 .unwrap();
816
817 assert!(config
819 .validation
820 .token_2022
821 .is_mint_extension_blocked(ExtensionType::TransferFeeConfig));
822 assert!(config.validation.token_2022.is_mint_extension_blocked(ExtensionType::Pausable));
823 assert!(!config
824 .validation
825 .token_2022
826 .is_mint_extension_blocked(ExtensionType::NonTransferable));
827
828 assert!(config
830 .validation
831 .token_2022
832 .is_account_extension_blocked(ExtensionType::MemoTransfer));
833 assert!(!config
834 .validation
835 .token_2022
836 .is_account_extension_blocked(ExtensionType::CpiGuard));
837 }
838
839 #[test]
840 fn test_cache_config_parsing() {
841 let config = ConfigBuilder::new()
842 .with_cache_config(Some("redis://localhost:6379"), true, 600, 120)
843 .build_config()
844 .unwrap();
845
846 assert_eq!(config.kora.cache.url, Some("redis://localhost:6379".to_string()));
847 assert!(config.kora.cache.enabled);
848 assert_eq!(config.kora.cache.default_ttl, 600);
849 assert_eq!(config.kora.cache.account_ttl, 120);
850 }
851
852 #[test]
853 fn test_cache_config_default() {
854 let config = ConfigBuilder::new().build_config().unwrap();
855
856 assert_eq!(config.kora.cache.url, None);
857 assert!(!config.kora.cache.enabled);
858 assert_eq!(config.kora.cache.default_ttl, 300);
859 assert_eq!(config.kora.cache.account_ttl, 60);
860 }
861
862 #[test]
863 fn test_max_request_body_size_default() {
864 let config = ConfigBuilder::new().build_config().unwrap();
865
866 assert_eq!(config.kora.max_request_body_size, DEFAULT_MAX_REQUEST_BODY_SIZE);
867 assert_eq!(config.kora.max_request_body_size, 2 * 1024 * 1024); }
869
870 #[test]
871 fn test_max_request_body_size_custom() {
872 let custom_size = 10 * 1024 * 1024; let config =
874 ConfigBuilder::new().with_max_request_body_size(custom_size).build_config().unwrap();
875
876 assert_eq!(config.kora.max_request_body_size, custom_size);
877 }
878}