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