Skip to main content

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