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