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