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