kora_lib/
config.rs

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)] // Default for backward compatibility
122    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    /// Allow fee payer to be the sender in System Transfer/TransferWithSeed instructions
152    pub allow_transfer: bool,
153    /// Allow fee payer to be the authority in System Assign/AssignWithSeed instructions
154    pub allow_assign: bool,
155    /// Allow fee payer to be the payer in System CreateAccount/CreateAccountWithSeed instructions
156    pub allow_create_account: bool,
157    /// Allow fee payer to be the account in System Allocate/AllocateWithSeed instructions
158    pub allow_allocate: bool,
159    /// Nested policy for nonce account operations
160    #[serde(default)]
161    pub nonce: NonceInstructionPolicy,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
165pub struct NonceInstructionPolicy {
166    /// Allow fee payer to be set as the nonce authority in InitializeNonceAccount instructions
167    pub allow_initialize: bool,
168    /// Allow fee payer to be the nonce authority in AdvanceNonceAccount instructions
169    pub allow_advance: bool,
170    /// Allow fee payer to be the nonce authority in WithdrawNonceAccount instructions
171    pub allow_withdraw: bool,
172    /// Allow fee payer to be the current nonce authority in AuthorizeNonceAccount instructions
173    pub allow_authorize: bool,
174    // Note: UpgradeNonceAccount not included - has no authority parameter, cannot validate fee payer involvement
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
178pub struct SplTokenInstructionPolicy {
179    /// Allow fee payer to be the owner in SPL Token Transfer/TransferChecked instructions
180    pub allow_transfer: bool,
181    /// Allow fee payer to be the owner in SPL Token Burn/BurnChecked instructions
182    pub allow_burn: bool,
183    /// Allow fee payer to be the owner in SPL Token CloseAccount instructions
184    pub allow_close_account: bool,
185    /// Allow fee payer to be the owner in SPL Token Approve/ApproveChecked instructions
186    pub allow_approve: bool,
187    /// Allow fee payer to be the owner in SPL Token Revoke instructions
188    pub allow_revoke: bool,
189    /// Allow fee payer to be the current authority in SPL Token SetAuthority instructions
190    pub allow_set_authority: bool,
191    /// Allow fee payer to be the mint authority in SPL Token MintTo/MintToChecked instructions
192    pub allow_mint_to: bool,
193    /// Allow fee payer to be the mint authority in SPL Token InitializeMint/InitializeMint2 instructions
194    pub allow_initialize_mint: bool,
195    /// Allow fee payer to be set as the owner in SPL Token InitializeAccount instructions
196    pub allow_initialize_account: bool,
197    /// Allow fee payer to be a signer in SPL Token InitializeMultisig instructions
198    pub allow_initialize_multisig: bool,
199    /// Allow fee payer to be the freeze authority in SPL Token FreezeAccount instructions
200    pub allow_freeze_account: bool,
201    /// Allow fee payer to be the freeze authority in SPL Token ThawAccount instructions
202    pub allow_thaw_account: bool,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
206pub struct Token2022InstructionPolicy {
207    /// Allow fee payer to be the owner in Token2022 Transfer/TransferChecked instructions
208    pub allow_transfer: bool,
209    /// Allow fee payer to be the owner in Token2022 Burn/BurnChecked instructions
210    pub allow_burn: bool,
211    /// Allow fee payer to be the owner in Token2022 CloseAccount instructions
212    pub allow_close_account: bool,
213    /// Allow fee payer to be the owner in Token2022 Approve/ApproveChecked instructions
214    pub allow_approve: bool,
215    /// Allow fee payer to be the owner in Token2022 Revoke instructions
216    pub allow_revoke: bool,
217    /// Allow fee payer to be the current authority in Token2022 SetAuthority instructions
218    pub allow_set_authority: bool,
219    /// Allow fee payer to be the mint authority in Token2022 MintTo/MintToChecked instructions
220    pub allow_mint_to: bool,
221    /// Allow fee payer to be the mint authority in Token2022 InitializeMint/InitializeMint2 instructions
222    pub allow_initialize_mint: bool,
223    /// Allow fee payer to be set as the owner in Token2022 InitializeAccount instructions
224    pub allow_initialize_account: bool,
225    /// Allow fee payer to be a signer in Token2022 InitializeMultisig instructions
226    pub allow_initialize_multisig: bool,
227    /// Allow fee payer to be the freeze authority in Token2022 FreezeAccount instructions
228    pub allow_freeze_account: bool,
229    /// Allow fee payer to be the freeze authority in Token2022 ThawAccount instructions
230    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    /// Initialize and parse extension strings into ExtensionTypes
256    /// This should be called after deserialization to populate the cached fields
257    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    /// Get all blocked mint extensions as ExtensionType
296    pub fn get_blocked_mint_extensions(&self) -> &[ExtensionType] {
297        self.parsed_blocked_mint_extensions.as_deref().unwrap_or(&[])
298    }
299
300    /// Get all blocked account extensions as ExtensionType
301    pub fn get_blocked_account_extensions(&self) -> &[ExtensionType] {
302        self.parsed_blocked_account_extensions.as_deref().unwrap_or(&[])
303    }
304
305    /// Check if a mint extension is blocked
306    pub fn is_mint_extension_blocked(&self, ext: ExtensionType) -> bool {
307        self.get_blocked_mint_extensions().contains(&ext)
308    }
309
310    /// Check if an account extension is blocked
311    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    /// Returns a Vec of enabled JSON-RPC method names
346    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    /// Redis URL for caching (e.g., "redis://localhost:6379")
426    pub url: Option<String>,
427    /// Enable caching for RPC calls
428    pub enabled: bool,
429    /// Default TTL for cached entries in seconds
430    pub default_ttl: u64,
431    /// TTL for account data cache in seconds
432    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    /// Optional payment address to receive payments (defaults to signer address)
456    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    /// Enable per-wallet usage limiting
480    pub enabled: bool,
481    /// Cache URL for shared usage limiting across multiple Kora instances
482    pub cache_url: Option<String>,
483    /// Default maximum transactions per wallet (0 = unlimited)
484    pub max_transactions: u64,
485    /// Fallback behavior when cache is unavailable
486    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        // Initialize Token2022Config to parse and cache extensions
531        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    /// Get the payment address from config or fallback to signer address
544    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                // Test passed
678            }
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        // Should default to Margin with 0.0 margin
688        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        // Test mint extension blocking
771        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        // Test account extension blocking
782        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); // 0 = unlimited
850    }
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); // 2 MB
858    }
859
860    #[test]
861    fn test_max_request_body_size_custom() {
862        let custom_size = 10 * 1024 * 1024; // 10 MB
863        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}