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