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 sign_and_send_bundle: bool,
332 #[serde(default)]
333 pub sign_bundle: bool,
334}
335
336impl EnabledMethods {
337 pub fn iter(&self) -> impl Iterator<Item = bool> {
338 [
339 self.liveness,
340 self.estimate_transaction_fee,
341 self.get_supported_tokens,
342 self.get_payer_signer,
343 self.sign_transaction,
344 self.sign_and_send_transaction,
345 self.transfer_transaction,
346 self.get_blockhash,
347 self.get_config,
348 self.get_version,
349 self.sign_and_send_bundle,
350 self.sign_bundle,
351 ]
352 .into_iter()
353 }
354
355 pub fn get_enabled_method_names(&self) -> Vec<String> {
357 let mut methods = Vec::new();
358 if self.liveness {
359 methods.push("liveness".to_string());
360 }
361 if self.estimate_transaction_fee {
362 methods.push("estimateTransactionFee".to_string());
363 }
364 if self.get_supported_tokens {
365 methods.push("getSupportedTokens".to_string());
366 }
367 if self.get_payer_signer {
368 methods.push("getPayerSigner".to_string());
369 }
370 if self.sign_transaction {
371 methods.push("signTransaction".to_string());
372 }
373 if self.sign_and_send_transaction {
374 methods.push("signAndSendTransaction".to_string());
375 }
376 if self.transfer_transaction {
377 methods.push("transferTransaction".to_string());
378 }
379 if self.get_blockhash {
380 methods.push("getBlockhash".to_string());
381 }
382 if self.get_config {
383 methods.push("getConfig".to_string());
384 }
385 if self.get_version {
386 methods.push("getVersion".to_string());
387 }
388 if self.sign_and_send_bundle {
389 methods.push("signAndSendBundle".to_string());
390 }
391 if self.sign_bundle {
392 methods.push("signBundle".to_string());
393 }
394 methods
395 }
396}
397
398impl IntoIterator for &EnabledMethods {
399 type Item = bool;
400 type IntoIter = std::array::IntoIter<bool, 12>;
401
402 fn into_iter(self) -> Self::IntoIter {
403 [
404 self.liveness,
405 self.estimate_transaction_fee,
406 self.get_supported_tokens,
407 self.get_payer_signer,
408 self.sign_transaction,
409 self.sign_and_send_transaction,
410 self.transfer_transaction,
411 self.get_blockhash,
412 self.get_config,
413 self.get_version,
414 self.sign_and_send_bundle,
415 self.sign_bundle,
416 ]
417 .into_iter()
418 }
419}
420
421impl Default for EnabledMethods {
422 fn default() -> Self {
423 Self {
424 liveness: true,
425 estimate_transaction_fee: true,
426 get_supported_tokens: true,
427 get_payer_signer: true,
428 sign_transaction: true,
429 sign_and_send_transaction: true,
430 transfer_transaction: true,
431 get_blockhash: true,
432 get_config: true,
433 get_version: true,
434 sign_and_send_bundle: false,
436 sign_bundle: false,
437 }
438 }
439}
440
441fn default_max_timestamp_age() -> i64 {
442 DEFAULT_MAX_TIMESTAMP_AGE
443}
444
445fn default_max_request_body_size() -> usize {
446 DEFAULT_MAX_REQUEST_BODY_SIZE
447}
448
449#[derive(Clone, Serialize, Deserialize, ToSchema)]
450pub struct CacheConfig {
451 pub url: Option<String>,
453 pub enabled: bool,
455 pub default_ttl: u64,
457 pub account_ttl: u64,
459}
460
461impl Default for CacheConfig {
462 fn default() -> Self {
463 Self {
464 url: None,
465 enabled: false,
466 default_ttl: DEFAULT_CACHE_DEFAULT_TTL,
467 account_ttl: DEFAULT_CACHE_ACCOUNT_TTL,
468 }
469 }
470}
471
472#[derive(Clone, Serialize, Deserialize, ToSchema)]
473pub struct KoraConfig {
474 pub rate_limit: u64,
475 #[serde(default = "default_max_request_body_size")]
476 pub max_request_body_size: usize,
477 #[serde(default)]
478 pub enabled_methods: EnabledMethods,
479 #[serde(default)]
480 pub auth: AuthConfig,
481 pub payment_address: Option<String>,
483 #[serde(default)]
484 pub cache: CacheConfig,
485 #[serde(default)]
486 pub usage_limit: UsageLimitConfig,
487 #[serde(default)]
489 pub bundle: BundleConfig,
490}
491
492impl Default for KoraConfig {
493 fn default() -> Self {
494 Self {
495 rate_limit: 100,
496 max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
497 enabled_methods: EnabledMethods::default(),
498 auth: AuthConfig::default(),
499 payment_address: None,
500 cache: CacheConfig::default(),
501 usage_limit: UsageLimitConfig::default(),
502 bundle: BundleConfig::default(),
503 }
504 }
505}
506
507#[derive(Clone, Serialize, Deserialize, ToSchema)]
508pub struct UsageLimitConfig {
509 pub enabled: bool,
511 pub cache_url: Option<String>,
513 pub max_transactions: u64,
515 pub fallback_if_unavailable: bool,
517}
518
519impl Default for UsageLimitConfig {
520 fn default() -> Self {
521 Self {
522 enabled: false,
523 cache_url: None,
524 max_transactions: DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS,
525 fallback_if_unavailable: DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE,
526 }
527 }
528}
529
530#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
532pub struct BundleConfig {
533 #[serde(default)]
535 pub enabled: bool,
536 #[serde(default)]
538 pub jito: JitoConfig,
539}
540
541#[derive(Clone, Serialize, Deserialize, ToSchema)]
542pub struct AuthConfig {
543 pub api_key: Option<String>,
544 pub hmac_secret: Option<String>,
545 #[serde(default = "default_max_timestamp_age")]
546 pub max_timestamp_age: i64,
547}
548
549impl Default for AuthConfig {
550 fn default() -> Self {
551 Self { api_key: None, hmac_secret: None, max_timestamp_age: DEFAULT_MAX_TIMESTAMP_AGE }
552 }
553}
554
555impl Config {
556 pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config, KoraError> {
557 let contents = fs::read_to_string(path).map_err(|e| {
558 KoraError::InternalServerError(format!(
559 "Failed to read config file: {}",
560 sanitize_error!(e)
561 ))
562 })?;
563
564 let mut config: Config = toml::from_str(&contents).map_err(|e| {
565 KoraError::InternalServerError(format!(
566 "Failed to parse config file: {}",
567 sanitize_error!(e)
568 ))
569 })?;
570
571 config.validation.token_2022.initialize().map_err(|e| {
573 KoraError::InternalServerError(format!(
574 "Failed to initialize Token2022 config: {}",
575 sanitize_error!(e)
576 ))
577 })?;
578
579 Ok(config)
580 }
581}
582
583impl KoraConfig {
584 pub fn get_payment_address(&self, signer_pubkey: &Pubkey) -> Result<Pubkey, KoraError> {
586 if let Some(payment_address_str) = &self.payment_address {
587 let payment_address = Pubkey::from_str(payment_address_str).map_err(|_| {
588 KoraError::InternalServerError("Invalid payment_address format".to_string())
589 })?;
590 Ok(payment_address)
591 } else {
592 Ok(*signer_pubkey)
593 }
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use crate::{
600 fee::price::PriceModel,
601 tests::toml_mock::{create_invalid_config, ConfigBuilder},
602 };
603
604 use super::*;
605
606 #[test]
607 fn test_load_valid_config() {
608 let config = ConfigBuilder::new()
609 .with_programs(vec!["program1", "program2"])
610 .with_tokens(vec!["token1", "token2"])
611 .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
612 .with_disallowed_accounts(vec!["account1"])
613 .build_config()
614 .unwrap();
615
616 assert_eq!(config.validation.max_allowed_lamports, 1000000000);
617 assert_eq!(config.validation.max_signatures, 10);
618 assert_eq!(config.validation.allowed_programs, vec!["program1", "program2"]);
619 assert_eq!(config.validation.allowed_tokens, vec!["token1", "token2"]);
620 assert_eq!(
621 config.validation.allowed_spl_paid_tokens,
622 SplTokenConfig::Allowlist(vec!["token3".to_string()])
623 );
624 assert_eq!(config.validation.disallowed_accounts, vec!["account1"]);
625 assert_eq!(config.validation.price_source, PriceSource::Jupiter);
626 assert_eq!(config.kora.rate_limit, 100);
627 assert!(config.kora.enabled_methods.estimate_transaction_fee);
628 assert!(config.kora.enabled_methods.sign_and_send_transaction);
629 }
630
631 #[test]
632 fn test_load_config_with_enabled_methods() {
633 let config = ConfigBuilder::new()
634 .with_programs(vec!["program1", "program2"])
635 .with_tokens(vec!["token1", "token2"])
636 .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
637 .with_disallowed_accounts(vec!["account1"])
638 .with_enabled_methods(&[
639 ("liveness", true),
640 ("estimate_transaction_fee", false),
641 ("get_supported_tokens", true),
642 ("sign_transaction", true),
643 ("sign_and_send_transaction", false),
644 ("transfer_transaction", true),
645 ("get_blockhash", true),
646 ("get_config", true),
647 ("get_payer_signer", true),
648 ("get_version", true),
649 ])
650 .build_config()
651 .unwrap();
652
653 assert_eq!(config.kora.rate_limit, 100);
654 assert!(config.kora.enabled_methods.liveness);
655 assert!(!config.kora.enabled_methods.estimate_transaction_fee);
656 assert!(config.kora.enabled_methods.get_supported_tokens);
657 assert!(config.kora.enabled_methods.sign_transaction);
658 assert!(!config.kora.enabled_methods.sign_and_send_transaction);
659 assert!(config.kora.enabled_methods.transfer_transaction);
660 assert!(config.kora.enabled_methods.get_blockhash);
661 assert!(config.kora.enabled_methods.get_config);
662 }
663
664 #[test]
665 fn test_load_invalid_config() {
666 let result = create_invalid_config("invalid toml content");
667 assert!(result.is_err());
668 }
669
670 #[test]
671 fn test_load_nonexistent_file() {
672 let result = Config::load_config("nonexistent_file.toml");
673 assert!(result.is_err());
674 }
675
676 #[test]
677 fn test_parse_spl_payment_config() {
678 let config =
679 ConfigBuilder::new().with_spl_paid_tokens(SplTokenConfig::All).build_config().unwrap();
680
681 assert_eq!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All);
682 }
683
684 #[test]
685 fn test_parse_margin_price_config() {
686 let config = ConfigBuilder::new().with_margin_price(0.1).build_config().unwrap();
687
688 match &config.validation.price.model {
689 PriceModel::Margin { margin } => {
690 assert_eq!(*margin, 0.1);
691 }
692 _ => panic!("Expected Margin price model"),
693 }
694 }
695
696 #[test]
697 fn test_parse_fixed_price_config() {
698 let config = ConfigBuilder::new()
699 .with_fixed_price(1000000, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")
700 .build_config()
701 .unwrap();
702
703 match &config.validation.price.model {
704 PriceModel::Fixed { amount, token, strict } => {
705 assert_eq!(*amount, 1000000);
706 assert_eq!(token, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
707 assert!(!strict);
708 }
709 _ => panic!("Expected Fixed price model"),
710 }
711 }
712
713 #[test]
714 fn test_parse_free_price_config() {
715 let config = ConfigBuilder::new().with_free_price().build_config().unwrap();
716
717 match &config.validation.price.model {
718 PriceModel::Free => {
719 }
721 _ => panic!("Expected Free price model"),
722 }
723 }
724
725 #[test]
726 fn test_parse_missing_price_config() {
727 let config = ConfigBuilder::new().build_config().unwrap();
728
729 match &config.validation.price.model {
731 PriceModel::Margin { margin } => {
732 assert_eq!(*margin, 0.0);
733 }
734 _ => panic!("Expected default Margin price model with 0.0 margin"),
735 }
736 }
737
738 #[test]
739 fn test_parse_invalid_price_config() {
740 let result = ConfigBuilder::new().with_invalid_price("invalid_type").build_config();
741
742 assert!(result.is_err());
743 if let Err(KoraError::InternalServerError(msg)) = result {
744 assert!(msg.contains("Failed to parse config file"));
745 } else {
746 panic!("Expected InternalServerError with parsing failure message");
747 }
748 }
749
750 #[test]
751 fn test_token2022_config_parsing() {
752 let config = ConfigBuilder::new()
753 .with_token2022_extensions(
754 vec!["transfer_fee_config", "pausable"],
755 vec!["memo_transfer", "cpi_guard"],
756 )
757 .build_config()
758 .unwrap();
759
760 assert_eq!(
761 config.validation.token_2022.blocked_mint_extensions,
762 vec!["transfer_fee_config", "pausable"]
763 );
764 assert_eq!(
765 config.validation.token_2022.blocked_account_extensions,
766 vec!["memo_transfer", "cpi_guard"]
767 );
768
769 let mint_extensions = config.validation.token_2022.get_blocked_mint_extensions();
770 assert_eq!(mint_extensions.len(), 2);
771
772 let account_extensions = config.validation.token_2022.get_blocked_account_extensions();
773 assert_eq!(account_extensions.len(), 2);
774 }
775
776 #[test]
777 fn test_token2022_config_invalid_extension() {
778 let result = ConfigBuilder::new()
779 .with_token2022_extensions(vec!["invalid_extension"], vec![])
780 .build_config();
781
782 assert!(result.is_err());
783 if let Err(KoraError::InternalServerError(msg)) = result {
784 assert!(msg.contains("Failed to initialize Token2022 config"));
785 assert!(msg.contains("Invalid mint extension name: 'invalid_extension'"));
786 } else {
787 panic!("Expected InternalServerError with Token2022 initialization failure");
788 }
789 }
790
791 #[test]
792 fn test_token2022_config_default() {
793 let config = ConfigBuilder::new().build_config().unwrap();
794
795 assert!(config.validation.token_2022.blocked_mint_extensions.is_empty());
796 assert!(config.validation.token_2022.blocked_account_extensions.is_empty());
797
798 assert!(config.validation.token_2022.get_blocked_mint_extensions().is_empty());
799 assert!(config.validation.token_2022.get_blocked_account_extensions().is_empty());
800 }
801
802 #[test]
803 fn test_token2022_extension_blocking_check() {
804 let config = ConfigBuilder::new()
805 .with_token2022_extensions(
806 vec!["transfer_fee_config", "pausable"],
807 vec!["memo_transfer"],
808 )
809 .build_config()
810 .unwrap();
811
812 assert!(config
814 .validation
815 .token_2022
816 .is_mint_extension_blocked(ExtensionType::TransferFeeConfig));
817 assert!(config.validation.token_2022.is_mint_extension_blocked(ExtensionType::Pausable));
818 assert!(!config
819 .validation
820 .token_2022
821 .is_mint_extension_blocked(ExtensionType::NonTransferable));
822
823 assert!(config
825 .validation
826 .token_2022
827 .is_account_extension_blocked(ExtensionType::MemoTransfer));
828 assert!(!config
829 .validation
830 .token_2022
831 .is_account_extension_blocked(ExtensionType::CpiGuard));
832 }
833
834 #[test]
835 fn test_cache_config_parsing() {
836 let config = ConfigBuilder::new()
837 .with_cache_config(Some("redis://localhost:6379"), true, 600, 120)
838 .build_config()
839 .unwrap();
840
841 assert_eq!(config.kora.cache.url, Some("redis://localhost:6379".to_string()));
842 assert!(config.kora.cache.enabled);
843 assert_eq!(config.kora.cache.default_ttl, 600);
844 assert_eq!(config.kora.cache.account_ttl, 120);
845 }
846
847 #[test]
848 fn test_cache_config_default() {
849 let config = ConfigBuilder::new().build_config().unwrap();
850
851 assert_eq!(config.kora.cache.url, None);
852 assert!(!config.kora.cache.enabled);
853 assert_eq!(config.kora.cache.default_ttl, 300);
854 assert_eq!(config.kora.cache.account_ttl, 60);
855 }
856
857 #[test]
858 fn test_usage_limit_config_parsing() {
859 let config = ConfigBuilder::new()
860 .with_usage_limit_config(true, Some("redis://localhost:6379"), 10, false)
861 .build_config()
862 .unwrap();
863
864 assert!(config.kora.usage_limit.enabled);
865 assert_eq!(config.kora.usage_limit.cache_url, Some("redis://localhost:6379".to_string()));
866 assert_eq!(config.kora.usage_limit.max_transactions, 10);
867 assert!(!config.kora.usage_limit.fallback_if_unavailable);
868 }
869
870 #[test]
871 fn test_usage_limit_config_default() {
872 let config = ConfigBuilder::new().build_config().unwrap();
873
874 assert!(!config.kora.usage_limit.enabled);
875 assert_eq!(config.kora.usage_limit.cache_url, None);
876 assert_eq!(config.kora.usage_limit.max_transactions, DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS);
877 assert_eq!(
878 config.kora.usage_limit.fallback_if_unavailable,
879 DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE
880 );
881 }
882
883 #[test]
884 fn test_usage_limit_config_unlimited() {
885 let config = ConfigBuilder::new()
886 .with_usage_limit_config(true, None, 0, true)
887 .build_config()
888 .unwrap();
889
890 assert!(config.kora.usage_limit.enabled);
891 assert_eq!(config.kora.usage_limit.max_transactions, 0); }
893
894 #[test]
895 fn test_max_request_body_size_default() {
896 let config = ConfigBuilder::new().build_config().unwrap();
897
898 assert_eq!(config.kora.max_request_body_size, DEFAULT_MAX_REQUEST_BODY_SIZE);
899 assert_eq!(config.kora.max_request_body_size, 2 * 1024 * 1024); }
901
902 #[test]
903 fn test_max_request_body_size_custom() {
904 let custom_size = 10 * 1024 * 1024; let config =
906 ConfigBuilder::new().with_max_request_body_size(custom_size).build_config().unwrap();
907
908 assert_eq!(config.kora.max_request_body_size, custom_size);
909 }
910}