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 #[serde(default)]
139 pub max_price_staleness_slots: u64,
140}
141
142impl ValidationConfig {
143 pub fn is_payment_required(&self) -> bool {
144 !matches!(&self.price.model, PriceModel::Free)
145 }
146
147 pub fn supports_token(&self, token: &str) -> bool {
148 self.allowed_spl_paid_tokens.has_token(token)
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
153#[serde(default)]
154pub struct FeePayerPolicy {
155 pub system: SystemInstructionPolicy,
156 pub spl_token: SplTokenInstructionPolicy,
157 pub token_2022: Token2022InstructionPolicy,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
161#[serde(default)]
162pub struct SystemInstructionPolicy {
163 pub allow_transfer: bool,
165 pub allow_assign: bool,
167 pub allow_create_account: bool,
169 pub allow_allocate: bool,
171 pub nonce: NonceInstructionPolicy,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
176pub struct NonceInstructionPolicy {
177 pub allow_initialize: bool,
179 pub allow_advance: bool,
181 pub allow_withdraw: bool,
183 pub allow_authorize: bool,
185 }
187
188#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
189pub struct SplTokenInstructionPolicy {
190 pub allow_transfer: bool,
192 pub allow_burn: bool,
194 pub allow_close_account: bool,
196 pub allow_approve: bool,
198 pub allow_revoke: bool,
200 pub allow_set_authority: bool,
202 pub allow_mint_to: bool,
204 pub allow_initialize_mint: bool,
206 pub allow_initialize_account: bool,
208 pub allow_initialize_multisig: bool,
210 pub allow_freeze_account: bool,
212 pub allow_thaw_account: bool,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
217pub struct Token2022InstructionPolicy {
218 pub allow_transfer: bool,
220 pub allow_burn: bool,
222 pub allow_close_account: bool,
224 pub allow_approve: bool,
226 pub allow_revoke: bool,
228 pub allow_set_authority: bool,
230 pub allow_mint_to: bool,
232 pub allow_initialize_mint: bool,
234 pub allow_initialize_account: bool,
236 pub allow_initialize_multisig: bool,
238 pub allow_freeze_account: bool,
240 pub allow_thaw_account: bool,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
245pub struct Token2022Config {
246 pub blocked_mint_extensions: Vec<String>,
247 pub blocked_account_extensions: Vec<String>,
248 #[serde(skip)]
249 parsed_blocked_mint_extensions: Option<Vec<ExtensionType>>,
250 #[serde(skip)]
251 parsed_blocked_account_extensions: Option<Vec<ExtensionType>>,
252}
253
254impl Default for Token2022Config {
255 fn default() -> Self {
256 Self {
257 blocked_mint_extensions: Vec::new(),
258 blocked_account_extensions: Vec::new(),
259 parsed_blocked_mint_extensions: Some(Vec::new()),
260 parsed_blocked_account_extensions: Some(Vec::new()),
261 }
262 }
263}
264
265impl Token2022Config {
266 pub fn initialize(&mut self) -> Result<(), String> {
269 let mut mint_extensions = Vec::new();
270 for name in &self.blocked_mint_extensions {
271 match crate::token::spl_token_2022_util::parse_mint_extension_string(name) {
272 Some(ext) => {
273 mint_extensions.push(ext);
274 }
275 None => {
276 return Err(format!(
277 "Invalid mint extension name: '{}'. Valid names are: {:?}",
278 name,
279 crate::token::spl_token_2022_util::get_all_mint_extension_names()
280 ));
281 }
282 }
283 }
284 self.parsed_blocked_mint_extensions = Some(mint_extensions);
285
286 let mut account_extensions = Vec::new();
287 for name in &self.blocked_account_extensions {
288 match crate::token::spl_token_2022_util::parse_account_extension_string(name) {
289 Some(ext) => {
290 account_extensions.push(ext);
291 }
292 None => {
293 return Err(format!(
294 "Invalid account extension name: '{}'. Valid names are: {:?}",
295 name,
296 crate::token::spl_token_2022_util::get_all_account_extension_names()
297 ));
298 }
299 }
300 }
301 self.parsed_blocked_account_extensions = Some(account_extensions);
302
303 Ok(())
304 }
305
306 pub fn get_blocked_mint_extensions(&self) -> &[ExtensionType] {
308 self.parsed_blocked_mint_extensions.as_deref().unwrap_or(&[])
309 }
310
311 pub fn get_blocked_account_extensions(&self) -> &[ExtensionType] {
313 self.parsed_blocked_account_extensions.as_deref().unwrap_or(&[])
314 }
315
316 pub fn is_mint_extension_blocked(&self, ext: ExtensionType) -> bool {
318 self.get_blocked_mint_extensions().contains(&ext)
319 }
320
321 pub fn is_account_extension_blocked(&self, ext: ExtensionType) -> bool {
323 self.get_blocked_account_extensions().contains(&ext)
324 }
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
328#[serde(default)]
329pub struct EnabledMethods {
330 pub liveness: bool,
331 pub estimate_transaction_fee: bool,
332 pub get_supported_tokens: bool,
333 pub get_payer_signer: bool,
334 pub sign_transaction: bool,
335 pub sign_and_send_transaction: bool,
336 pub transfer_transaction: bool,
337 pub get_blockhash: bool,
338 pub get_config: bool,
339 pub get_version: bool,
340 pub estimate_bundle_fee: bool,
342 pub sign_and_send_bundle: bool,
343 pub sign_bundle: bool,
344}
345
346impl EnabledMethods {
347 pub fn iter(&self) -> impl Iterator<Item = bool> {
348 [
349 self.liveness,
350 self.estimate_transaction_fee,
351 self.get_supported_tokens,
352 self.get_payer_signer,
353 self.sign_transaction,
354 self.sign_and_send_transaction,
355 self.transfer_transaction,
356 self.get_blockhash,
357 self.get_config,
358 self.get_version,
359 self.estimate_bundle_fee,
360 self.sign_and_send_bundle,
361 self.sign_bundle,
362 ]
363 .into_iter()
364 }
365
366 pub fn get_enabled_method_names(&self) -> Vec<String> {
368 let mut methods = Vec::new();
369 if self.liveness {
370 methods.push("liveness".to_string());
371 }
372 if self.estimate_transaction_fee {
373 methods.push("estimateTransactionFee".to_string());
374 }
375 if self.estimate_bundle_fee {
376 methods.push("estimateBundleFee".to_string());
377 }
378 if self.get_supported_tokens {
379 methods.push("getSupportedTokens".to_string());
380 }
381 if self.get_payer_signer {
382 methods.push("getPayerSigner".to_string());
383 }
384 if self.sign_transaction {
385 methods.push("signTransaction".to_string());
386 }
387 if self.sign_and_send_transaction {
388 methods.push("signAndSendTransaction".to_string());
389 }
390 if self.transfer_transaction {
391 methods.push("transferTransaction".to_string());
392 }
393 if self.get_blockhash {
394 methods.push("getBlockhash".to_string());
395 }
396 if self.get_config {
397 methods.push("getConfig".to_string());
398 }
399 if self.get_version {
400 methods.push("getVersion".to_string());
401 }
402 if self.sign_and_send_bundle {
403 methods.push("signAndSendBundle".to_string());
404 }
405 if self.sign_bundle {
406 methods.push("signBundle".to_string());
407 }
408 methods
409 }
410}
411
412impl IntoIterator for &EnabledMethods {
413 type Item = bool;
414 type IntoIter = std::array::IntoIter<bool, 13>;
415
416 fn into_iter(self) -> Self::IntoIter {
417 [
418 self.liveness,
419 self.estimate_transaction_fee,
420 self.get_supported_tokens,
421 self.get_payer_signer,
422 self.sign_transaction,
423 self.sign_and_send_transaction,
424 self.transfer_transaction,
425 self.get_blockhash,
426 self.get_config,
427 self.get_version,
428 self.estimate_bundle_fee,
429 self.sign_and_send_bundle,
430 self.sign_bundle,
431 ]
432 .into_iter()
433 }
434}
435
436impl Default for EnabledMethods {
437 fn default() -> Self {
438 Self {
439 liveness: true,
440 estimate_transaction_fee: true,
441 get_supported_tokens: true,
442 get_payer_signer: true,
443 sign_transaction: true,
444 sign_and_send_transaction: true,
445 transfer_transaction: true,
446 get_blockhash: true,
447 get_config: true,
448 get_version: true,
449 estimate_bundle_fee: false,
451 sign_and_send_bundle: false,
452 sign_bundle: false,
453 }
454 }
455}
456
457#[derive(Clone, Serialize, Deserialize, ToSchema)]
458pub struct CacheConfig {
459 pub url: Option<String>,
461 pub enabled: bool,
463 pub default_ttl: u64,
465 pub account_ttl: u64,
467}
468
469impl Default for CacheConfig {
470 fn default() -> Self {
471 Self {
472 url: None,
473 enabled: false,
474 default_ttl: DEFAULT_CACHE_DEFAULT_TTL,
475 account_ttl: DEFAULT_CACHE_ACCOUNT_TTL,
476 }
477 }
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq, Hash)]
481#[serde(rename_all = "snake_case")]
482pub enum TransactionPluginType {
483 GasSwap,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
487#[serde(default)]
488pub struct PluginsConfig {
489 pub enabled: Vec<TransactionPluginType>,
491}
492
493#[derive(Clone, Serialize, Deserialize, ToSchema)]
494#[serde(default)]
495pub struct KoraConfig {
496 pub rate_limit: u64,
497 pub max_request_body_size: usize,
498 pub enabled_methods: EnabledMethods,
499 pub auth: AuthConfig,
500 pub payment_address: Option<String>,
502 pub cache: CacheConfig,
503 pub usage_limit: UsageLimitConfig,
504 pub plugins: PluginsConfig,
506 pub bundle: BundleConfig,
508 pub lighthouse: LighthouseConfig,
510 pub force_sig_verify: bool,
513}
514
515impl Default for KoraConfig {
516 fn default() -> Self {
517 Self {
518 rate_limit: 100,
519 max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
520 enabled_methods: EnabledMethods::default(),
521 auth: AuthConfig::default(),
522 payment_address: None,
523 cache: CacheConfig::default(),
524 usage_limit: UsageLimitConfig::default(),
525 plugins: PluginsConfig::default(),
526 bundle: BundleConfig::default(),
527 lighthouse: LighthouseConfig::default(),
528 force_sig_verify: false,
529 }
530 }
531}
532
533#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
535#[serde(default)]
536pub struct BundleConfig {
537 pub enabled: bool,
539 pub jito: JitoConfig,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
545#[serde(default)]
546pub struct LighthouseConfig {
547 pub enabled: bool,
549 pub fail_if_transaction_size_overflow: bool,
552}
553
554impl Default for LighthouseConfig {
555 fn default() -> Self {
556 Self { enabled: false, fail_if_transaction_size_overflow: true }
557 }
558}
559
560#[derive(Clone, Serialize, Deserialize, ToSchema)]
561#[serde(default)]
562pub struct AuthConfig {
563 pub api_key: Option<String>,
564 pub hmac_secret: Option<String>,
565 pub recaptcha_secret: Option<String>,
566 pub recaptcha_score_threshold: f64,
567 pub max_timestamp_age: i64,
568 pub protected_methods: Vec<String>,
569}
570
571impl Default for AuthConfig {
572 fn default() -> Self {
573 Self {
574 api_key: None,
575 hmac_secret: None,
576 recaptcha_secret: None,
577 recaptcha_score_threshold: DEFAULT_RECAPTCHA_SCORE_THRESHOLD,
578 max_timestamp_age: DEFAULT_MAX_TIMESTAMP_AGE,
579 protected_methods: DEFAULT_PROTECTED_METHODS.iter().map(|s| s.to_string()).collect(),
580 }
581 }
582}
583
584impl Config {
585 pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config, KoraError> {
586 let contents = fs::read_to_string(path).map_err(|e| {
587 KoraError::InternalServerError(format!(
588 "Failed to read config file: {}",
589 sanitize_error!(e)
590 ))
591 })?;
592
593 let mut config: Config = toml::from_str(&contents).map_err(|e| {
594 KoraError::InternalServerError(format!(
595 "Failed to parse config file: {}",
596 sanitize_error!(e)
597 ))
598 })?;
599
600 config.validation.token_2022.initialize().map_err(|e| {
602 KoraError::InternalServerError(format!(
603 "Failed to initialize Token2022 config: {}",
604 sanitize_error!(e)
605 ))
606 })?;
607
608 Ok(config)
609 }
610}
611
612impl KoraConfig {
613 pub fn get_payment_address(&self, signer_pubkey: &Pubkey) -> Result<Pubkey, KoraError> {
615 if let Some(payment_address_str) = &self.payment_address {
616 let payment_address = Pubkey::from_str(payment_address_str).map_err(|_| {
617 KoraError::InternalServerError("Invalid payment_address format".to_string())
618 })?;
619 Ok(payment_address)
620 } else {
621 Ok(*signer_pubkey)
622 }
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use crate::{
629 fee::price::PriceModel,
630 tests::toml_mock::{create_invalid_config, ConfigBuilder},
631 };
632
633 use super::*;
634
635 #[test]
636 fn test_load_valid_config() {
637 let config = ConfigBuilder::new()
638 .with_programs(vec!["program1", "program2"])
639 .with_tokens(vec!["token1", "token2"])
640 .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
641 .with_disallowed_accounts(vec!["account1"])
642 .build_config()
643 .unwrap();
644
645 assert_eq!(config.validation.max_allowed_lamports, 1000000000);
646 assert_eq!(config.validation.max_signatures, 10);
647 assert_eq!(config.validation.allowed_programs, vec!["program1", "program2"]);
648 assert_eq!(config.validation.allowed_tokens, vec!["token1", "token2"]);
649 assert_eq!(
650 config.validation.allowed_spl_paid_tokens,
651 SplTokenConfig::Allowlist(vec!["token3".to_string()])
652 );
653 assert_eq!(config.validation.disallowed_accounts, vec!["account1"]);
654 assert_eq!(config.validation.price_source, PriceSource::Jupiter);
655 assert_eq!(config.kora.rate_limit, 100);
656 assert!(config.kora.enabled_methods.estimate_transaction_fee);
657 assert!(config.kora.enabled_methods.sign_and_send_transaction);
658 }
659
660 #[test]
661 fn test_load_config_with_enabled_methods() {
662 let config = ConfigBuilder::new()
663 .with_programs(vec!["program1", "program2"])
664 .with_tokens(vec!["token1", "token2"])
665 .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
666 .with_disallowed_accounts(vec!["account1"])
667 .with_enabled_methods(&[
668 ("liveness", true),
669 ("estimate_transaction_fee", false),
670 ("get_supported_tokens", true),
671 ("sign_transaction", true),
672 ("sign_and_send_transaction", false),
673 ("transfer_transaction", true),
674 ("get_blockhash", true),
675 ("get_config", true),
676 ("get_payer_signer", true),
677 ("get_version", true),
678 ])
679 .build_config()
680 .unwrap();
681
682 assert_eq!(config.kora.rate_limit, 100);
683 assert!(config.kora.enabled_methods.liveness);
684 assert!(!config.kora.enabled_methods.estimate_transaction_fee);
685 assert!(config.kora.enabled_methods.get_supported_tokens);
686 assert!(config.kora.enabled_methods.sign_transaction);
687 assert!(!config.kora.enabled_methods.sign_and_send_transaction);
688 assert!(config.kora.enabled_methods.transfer_transaction);
689 assert!(config.kora.enabled_methods.get_blockhash);
690 assert!(config.kora.enabled_methods.get_config);
691 }
692
693 #[test]
694 fn test_load_invalid_config() {
695 let result = create_invalid_config("invalid toml content");
696 assert!(result.is_err());
697 }
698
699 #[test]
700 fn test_load_nonexistent_file() {
701 let result = Config::load_config("nonexistent_file.toml");
702 assert!(result.is_err());
703 }
704
705 #[test]
706 fn test_parse_spl_payment_config() {
707 let config =
708 ConfigBuilder::new().with_spl_paid_tokens(SplTokenConfig::All).build_config().unwrap();
709
710 assert_eq!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All);
711 }
712
713 #[test]
714 fn test_parse_margin_price_config() {
715 let config = ConfigBuilder::new().with_margin_price(0.1).build_config().unwrap();
716
717 match &config.validation.price.model {
718 PriceModel::Margin { margin } => {
719 assert_eq!(*margin, 0.1);
720 }
721 _ => panic!("Expected Margin price model"),
722 }
723 }
724
725 #[test]
726 fn test_parse_fixed_price_config() {
727 let config = ConfigBuilder::new()
728 .with_fixed_price(1000000, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")
729 .build_config()
730 .unwrap();
731
732 match &config.validation.price.model {
733 PriceModel::Fixed { amount, token, strict } => {
734 assert_eq!(*amount, 1000000);
735 assert_eq!(token, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
736 assert!(!strict);
737 }
738 _ => panic!("Expected Fixed price model"),
739 }
740 }
741
742 #[test]
743 fn test_parse_free_price_config() {
744 let config = ConfigBuilder::new().with_free_price().build_config().unwrap();
745
746 match &config.validation.price.model {
747 PriceModel::Free => {
748 }
750 _ => panic!("Expected Free price model"),
751 }
752 }
753
754 #[test]
755 fn test_parse_missing_price_config() {
756 let config = ConfigBuilder::new().build_config().unwrap();
757
758 match &config.validation.price.model {
760 PriceModel::Margin { margin } => {
761 assert_eq!(*margin, 0.0);
762 }
763 _ => panic!("Expected default Margin price model with 0.0 margin"),
764 }
765 }
766
767 #[test]
768 fn test_parse_invalid_price_config() {
769 let result = ConfigBuilder::new().with_invalid_price("invalid_type").build_config();
770
771 assert!(result.is_err());
772 if let Err(KoraError::InternalServerError(msg)) = result {
773 assert!(msg.contains("Failed to parse config file"));
774 } else {
775 panic!("Expected InternalServerError with parsing failure message");
776 }
777 }
778
779 #[test]
780 fn test_token2022_config_parsing() {
781 let config = ConfigBuilder::new()
782 .with_token2022_extensions(
783 vec!["transfer_fee_config", "pausable"],
784 vec!["memo_transfer", "cpi_guard"],
785 )
786 .build_config()
787 .unwrap();
788
789 assert_eq!(
790 config.validation.token_2022.blocked_mint_extensions,
791 vec!["transfer_fee_config", "pausable"]
792 );
793 assert_eq!(
794 config.validation.token_2022.blocked_account_extensions,
795 vec!["memo_transfer", "cpi_guard"]
796 );
797
798 let mint_extensions = config.validation.token_2022.get_blocked_mint_extensions();
799 assert_eq!(mint_extensions.len(), 2);
800
801 let account_extensions = config.validation.token_2022.get_blocked_account_extensions();
802 assert_eq!(account_extensions.len(), 2);
803 }
804
805 #[test]
806 fn test_token2022_config_invalid_extension() {
807 let result = ConfigBuilder::new()
808 .with_token2022_extensions(vec!["invalid_extension"], vec![])
809 .build_config();
810
811 assert!(result.is_err());
812 if let Err(KoraError::InternalServerError(msg)) = result {
813 assert!(msg.contains("Failed to initialize Token2022 config"));
814 assert!(msg.contains("Invalid mint extension name: 'invalid_extension'"));
815 } else {
816 panic!("Expected InternalServerError with Token2022 initialization failure");
817 }
818 }
819
820 #[test]
821 fn test_token2022_config_default() {
822 let config = ConfigBuilder::new().build_config().unwrap();
823
824 assert!(config.validation.token_2022.blocked_mint_extensions.is_empty());
825 assert!(config.validation.token_2022.blocked_account_extensions.is_empty());
826
827 assert!(config.validation.token_2022.get_blocked_mint_extensions().is_empty());
828 assert!(config.validation.token_2022.get_blocked_account_extensions().is_empty());
829 }
830
831 #[test]
832 fn test_token2022_extension_blocking_check() {
833 let config = ConfigBuilder::new()
834 .with_token2022_extensions(
835 vec!["transfer_fee_config", "pausable"],
836 vec!["memo_transfer"],
837 )
838 .build_config()
839 .unwrap();
840
841 assert!(config
843 .validation
844 .token_2022
845 .is_mint_extension_blocked(ExtensionType::TransferFeeConfig));
846 assert!(config.validation.token_2022.is_mint_extension_blocked(ExtensionType::Pausable));
847 assert!(!config
848 .validation
849 .token_2022
850 .is_mint_extension_blocked(ExtensionType::NonTransferable));
851
852 assert!(config
854 .validation
855 .token_2022
856 .is_account_extension_blocked(ExtensionType::MemoTransfer));
857 assert!(!config
858 .validation
859 .token_2022
860 .is_account_extension_blocked(ExtensionType::CpiGuard));
861 }
862
863 #[test]
864 fn test_cache_config_parsing() {
865 let config = ConfigBuilder::new()
866 .with_cache_config(Some("redis://localhost:6379"), true, 600, 120)
867 .build_config()
868 .unwrap();
869
870 assert_eq!(config.kora.cache.url, Some("redis://localhost:6379".to_string()));
871 assert!(config.kora.cache.enabled);
872 assert_eq!(config.kora.cache.default_ttl, 600);
873 assert_eq!(config.kora.cache.account_ttl, 120);
874 }
875
876 #[test]
877 fn test_cache_config_default() {
878 let config = ConfigBuilder::new().build_config().unwrap();
879
880 assert_eq!(config.kora.cache.url, None);
881 assert!(!config.kora.cache.enabled);
882 assert_eq!(config.kora.cache.default_ttl, 300);
883 assert_eq!(config.kora.cache.account_ttl, 60);
884 }
885
886 #[test]
887 fn test_max_request_body_size_default() {
888 let config = ConfigBuilder::new().build_config().unwrap();
889
890 assert_eq!(config.kora.max_request_body_size, DEFAULT_MAX_REQUEST_BODY_SIZE);
891 assert_eq!(config.kora.max_request_body_size, 2 * 1024 * 1024); }
893
894 #[test]
895 fn test_max_request_body_size_custom() {
896 let custom_size = 10 * 1024 * 1024; let config =
898 ConfigBuilder::new().with_max_request_body_size(custom_size).build_config().unwrap();
899
900 assert_eq!(config.kora.max_request_body_size, custom_size);
901 }
902}