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 estimate_bundle_fee: bool,
332    #[serde(default)]
333    pub sign_and_send_bundle: bool,
334    #[serde(default)]
335    pub sign_bundle: bool,
336}
337
338impl EnabledMethods {
339    pub fn iter(&self) -> impl Iterator<Item = bool> {
340        [
341            self.liveness,
342            self.estimate_transaction_fee,
343            self.get_supported_tokens,
344            self.get_payer_signer,
345            self.sign_transaction,
346            self.sign_and_send_transaction,
347            self.transfer_transaction,
348            self.get_blockhash,
349            self.get_config,
350            self.get_version,
351            self.estimate_bundle_fee,
352            self.sign_and_send_bundle,
353            self.sign_bundle,
354        ]
355        .into_iter()
356    }
357
358    /// Returns a Vec of enabled JSON-RPC method names
359    pub fn get_enabled_method_names(&self) -> Vec<String> {
360        let mut methods = Vec::new();
361        if self.liveness {
362            methods.push("liveness".to_string());
363        }
364        if self.estimate_transaction_fee {
365            methods.push("estimateTransactionFee".to_string());
366        }
367        if self.estimate_bundle_fee {
368            methods.push("estimateBundleFee".to_string());
369        }
370        if self.get_supported_tokens {
371            methods.push("getSupportedTokens".to_string());
372        }
373        if self.get_payer_signer {
374            methods.push("getPayerSigner".to_string());
375        }
376        if self.sign_transaction {
377            methods.push("signTransaction".to_string());
378        }
379        if self.sign_and_send_transaction {
380            methods.push("signAndSendTransaction".to_string());
381        }
382        if self.transfer_transaction {
383            methods.push("transferTransaction".to_string());
384        }
385        if self.get_blockhash {
386            methods.push("getBlockhash".to_string());
387        }
388        if self.get_config {
389            methods.push("getConfig".to_string());
390        }
391        if self.get_version {
392            methods.push("getVersion".to_string());
393        }
394        if self.sign_and_send_bundle {
395            methods.push("signAndSendBundle".to_string());
396        }
397        if self.sign_bundle {
398            methods.push("signBundle".to_string());
399        }
400        methods
401    }
402}
403
404impl IntoIterator for &EnabledMethods {
405    type Item = bool;
406    type IntoIter = std::array::IntoIter<bool, 13>;
407
408    fn into_iter(self) -> Self::IntoIter {
409        [
410            self.liveness,
411            self.estimate_transaction_fee,
412            self.get_supported_tokens,
413            self.get_payer_signer,
414            self.sign_transaction,
415            self.sign_and_send_transaction,
416            self.transfer_transaction,
417            self.get_blockhash,
418            self.get_config,
419            self.get_version,
420            self.estimate_bundle_fee,
421            self.sign_and_send_bundle,
422            self.sign_bundle,
423        ]
424        .into_iter()
425    }
426}
427
428impl Default for EnabledMethods {
429    fn default() -> Self {
430        Self {
431            liveness: true,
432            estimate_transaction_fee: true,
433            get_supported_tokens: true,
434            get_payer_signer: true,
435            sign_transaction: true,
436            sign_and_send_transaction: true,
437            transfer_transaction: true,
438            get_blockhash: true,
439            get_config: true,
440            get_version: true,
441            // Bundle methods default to false (opt-in)
442            estimate_bundle_fee: false,
443            sign_and_send_bundle: false,
444            sign_bundle: false,
445        }
446    }
447}
448
449fn default_max_timestamp_age() -> i64 {
450    DEFAULT_MAX_TIMESTAMP_AGE
451}
452
453fn default_max_request_body_size() -> usize {
454    DEFAULT_MAX_REQUEST_BODY_SIZE
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(Clone, Serialize, Deserialize, ToSchema)]
481pub struct KoraConfig {
482    pub rate_limit: u64,
483    #[serde(default = "default_max_request_body_size")]
484    pub max_request_body_size: usize,
485    #[serde(default)]
486    pub enabled_methods: EnabledMethods,
487    #[serde(default)]
488    pub auth: AuthConfig,
489    /// Optional payment address to receive payments (defaults to signer address)
490    pub payment_address: Option<String>,
491    #[serde(default)]
492    pub cache: CacheConfig,
493    #[serde(default)]
494    pub usage_limit: UsageLimitConfig,
495    /// Bundle support configuration
496    #[serde(default)]
497    pub bundle: BundleConfig,
498}
499
500impl Default for KoraConfig {
501    fn default() -> Self {
502        Self {
503            rate_limit: 100,
504            max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE,
505            enabled_methods: EnabledMethods::default(),
506            auth: AuthConfig::default(),
507            payment_address: None,
508            cache: CacheConfig::default(),
509            usage_limit: UsageLimitConfig::default(),
510            bundle: BundleConfig::default(),
511        }
512    }
513}
514
515#[derive(Clone, Serialize, Deserialize, ToSchema)]
516pub struct UsageLimitConfig {
517    /// Enable per-wallet usage limiting
518    pub enabled: bool,
519    /// Cache URL for shared usage limiting across multiple Kora instances
520    pub cache_url: Option<String>,
521    /// Default maximum transactions per wallet (0 = unlimited)
522    pub max_transactions: u64,
523    /// Fallback behavior when cache is unavailable
524    pub fallback_if_unavailable: bool,
525}
526
527impl Default for UsageLimitConfig {
528    fn default() -> Self {
529        Self {
530            enabled: false,
531            cache_url: None,
532            max_transactions: DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS,
533            fallback_if_unavailable: DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE,
534        }
535    }
536}
537
538/// Configuration for bundle support (wraps provider-specific configs)
539#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
540pub struct BundleConfig {
541    /// Enable bundle support
542    #[serde(default)]
543    pub enabled: bool,
544    /// Jito-specific configuration
545    #[serde(default)]
546    pub jito: JitoConfig,
547}
548
549#[derive(Clone, Serialize, Deserialize, ToSchema)]
550pub struct AuthConfig {
551    pub api_key: Option<String>,
552    pub hmac_secret: Option<String>,
553    #[serde(default = "default_max_timestamp_age")]
554    pub max_timestamp_age: i64,
555}
556
557impl Default for AuthConfig {
558    fn default() -> Self {
559        Self { api_key: None, hmac_secret: None, max_timestamp_age: DEFAULT_MAX_TIMESTAMP_AGE }
560    }
561}
562
563impl Config {
564    pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config, KoraError> {
565        let contents = fs::read_to_string(path).map_err(|e| {
566            KoraError::InternalServerError(format!(
567                "Failed to read config file: {}",
568                sanitize_error!(e)
569            ))
570        })?;
571
572        let mut config: Config = toml::from_str(&contents).map_err(|e| {
573            KoraError::InternalServerError(format!(
574                "Failed to parse config file: {}",
575                sanitize_error!(e)
576            ))
577        })?;
578
579        // Initialize Token2022Config to parse and cache extensions
580        config.validation.token_2022.initialize().map_err(|e| {
581            KoraError::InternalServerError(format!(
582                "Failed to initialize Token2022 config: {}",
583                sanitize_error!(e)
584            ))
585        })?;
586
587        Ok(config)
588    }
589}
590
591impl KoraConfig {
592    /// Get the payment address from config or fallback to signer address
593    pub fn get_payment_address(&self, signer_pubkey: &Pubkey) -> Result<Pubkey, KoraError> {
594        if let Some(payment_address_str) = &self.payment_address {
595            let payment_address = Pubkey::from_str(payment_address_str).map_err(|_| {
596                KoraError::InternalServerError("Invalid payment_address format".to_string())
597            })?;
598            Ok(payment_address)
599        } else {
600            Ok(*signer_pubkey)
601        }
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use crate::{
608        fee::price::PriceModel,
609        tests::toml_mock::{create_invalid_config, ConfigBuilder},
610    };
611
612    use super::*;
613
614    #[test]
615    fn test_load_valid_config() {
616        let config = ConfigBuilder::new()
617            .with_programs(vec!["program1", "program2"])
618            .with_tokens(vec!["token1", "token2"])
619            .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
620            .with_disallowed_accounts(vec!["account1"])
621            .build_config()
622            .unwrap();
623
624        assert_eq!(config.validation.max_allowed_lamports, 1000000000);
625        assert_eq!(config.validation.max_signatures, 10);
626        assert_eq!(config.validation.allowed_programs, vec!["program1", "program2"]);
627        assert_eq!(config.validation.allowed_tokens, vec!["token1", "token2"]);
628        assert_eq!(
629            config.validation.allowed_spl_paid_tokens,
630            SplTokenConfig::Allowlist(vec!["token3".to_string()])
631        );
632        assert_eq!(config.validation.disallowed_accounts, vec!["account1"]);
633        assert_eq!(config.validation.price_source, PriceSource::Jupiter);
634        assert_eq!(config.kora.rate_limit, 100);
635        assert!(config.kora.enabled_methods.estimate_transaction_fee);
636        assert!(config.kora.enabled_methods.sign_and_send_transaction);
637    }
638
639    #[test]
640    fn test_load_config_with_enabled_methods() {
641        let config = ConfigBuilder::new()
642            .with_programs(vec!["program1", "program2"])
643            .with_tokens(vec!["token1", "token2"])
644            .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec!["token3".to_string()]))
645            .with_disallowed_accounts(vec!["account1"])
646            .with_enabled_methods(&[
647                ("liveness", true),
648                ("estimate_transaction_fee", false),
649                ("get_supported_tokens", true),
650                ("sign_transaction", true),
651                ("sign_and_send_transaction", false),
652                ("transfer_transaction", true),
653                ("get_blockhash", true),
654                ("get_config", true),
655                ("get_payer_signer", true),
656                ("get_version", true),
657            ])
658            .build_config()
659            .unwrap();
660
661        assert_eq!(config.kora.rate_limit, 100);
662        assert!(config.kora.enabled_methods.liveness);
663        assert!(!config.kora.enabled_methods.estimate_transaction_fee);
664        assert!(config.kora.enabled_methods.get_supported_tokens);
665        assert!(config.kora.enabled_methods.sign_transaction);
666        assert!(!config.kora.enabled_methods.sign_and_send_transaction);
667        assert!(config.kora.enabled_methods.transfer_transaction);
668        assert!(config.kora.enabled_methods.get_blockhash);
669        assert!(config.kora.enabled_methods.get_config);
670    }
671
672    #[test]
673    fn test_load_invalid_config() {
674        let result = create_invalid_config("invalid toml content");
675        assert!(result.is_err());
676    }
677
678    #[test]
679    fn test_load_nonexistent_file() {
680        let result = Config::load_config("nonexistent_file.toml");
681        assert!(result.is_err());
682    }
683
684    #[test]
685    fn test_parse_spl_payment_config() {
686        let config =
687            ConfigBuilder::new().with_spl_paid_tokens(SplTokenConfig::All).build_config().unwrap();
688
689        assert_eq!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All);
690    }
691
692    #[test]
693    fn test_parse_margin_price_config() {
694        let config = ConfigBuilder::new().with_margin_price(0.1).build_config().unwrap();
695
696        match &config.validation.price.model {
697            PriceModel::Margin { margin } => {
698                assert_eq!(*margin, 0.1);
699            }
700            _ => panic!("Expected Margin price model"),
701        }
702    }
703
704    #[test]
705    fn test_parse_fixed_price_config() {
706        let config = ConfigBuilder::new()
707            .with_fixed_price(1000000, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")
708            .build_config()
709            .unwrap();
710
711        match &config.validation.price.model {
712            PriceModel::Fixed { amount, token, strict } => {
713                assert_eq!(*amount, 1000000);
714                assert_eq!(token, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
715                assert!(!strict);
716            }
717            _ => panic!("Expected Fixed price model"),
718        }
719    }
720
721    #[test]
722    fn test_parse_free_price_config() {
723        let config = ConfigBuilder::new().with_free_price().build_config().unwrap();
724
725        match &config.validation.price.model {
726            PriceModel::Free => {
727                // Test passed
728            }
729            _ => panic!("Expected Free price model"),
730        }
731    }
732
733    #[test]
734    fn test_parse_missing_price_config() {
735        let config = ConfigBuilder::new().build_config().unwrap();
736
737        // Should default to Margin with 0.0 margin
738        match &config.validation.price.model {
739            PriceModel::Margin { margin } => {
740                assert_eq!(*margin, 0.0);
741            }
742            _ => panic!("Expected default Margin price model with 0.0 margin"),
743        }
744    }
745
746    #[test]
747    fn test_parse_invalid_price_config() {
748        let result = ConfigBuilder::new().with_invalid_price("invalid_type").build_config();
749
750        assert!(result.is_err());
751        if let Err(KoraError::InternalServerError(msg)) = result {
752            assert!(msg.contains("Failed to parse config file"));
753        } else {
754            panic!("Expected InternalServerError with parsing failure message");
755        }
756    }
757
758    #[test]
759    fn test_token2022_config_parsing() {
760        let config = ConfigBuilder::new()
761            .with_token2022_extensions(
762                vec!["transfer_fee_config", "pausable"],
763                vec!["memo_transfer", "cpi_guard"],
764            )
765            .build_config()
766            .unwrap();
767
768        assert_eq!(
769            config.validation.token_2022.blocked_mint_extensions,
770            vec!["transfer_fee_config", "pausable"]
771        );
772        assert_eq!(
773            config.validation.token_2022.blocked_account_extensions,
774            vec!["memo_transfer", "cpi_guard"]
775        );
776
777        let mint_extensions = config.validation.token_2022.get_blocked_mint_extensions();
778        assert_eq!(mint_extensions.len(), 2);
779
780        let account_extensions = config.validation.token_2022.get_blocked_account_extensions();
781        assert_eq!(account_extensions.len(), 2);
782    }
783
784    #[test]
785    fn test_token2022_config_invalid_extension() {
786        let result = ConfigBuilder::new()
787            .with_token2022_extensions(vec!["invalid_extension"], vec![])
788            .build_config();
789
790        assert!(result.is_err());
791        if let Err(KoraError::InternalServerError(msg)) = result {
792            assert!(msg.contains("Failed to initialize Token2022 config"));
793            assert!(msg.contains("Invalid mint extension name: 'invalid_extension'"));
794        } else {
795            panic!("Expected InternalServerError with Token2022 initialization failure");
796        }
797    }
798
799    #[test]
800    fn test_token2022_config_default() {
801        let config = ConfigBuilder::new().build_config().unwrap();
802
803        assert!(config.validation.token_2022.blocked_mint_extensions.is_empty());
804        assert!(config.validation.token_2022.blocked_account_extensions.is_empty());
805
806        assert!(config.validation.token_2022.get_blocked_mint_extensions().is_empty());
807        assert!(config.validation.token_2022.get_blocked_account_extensions().is_empty());
808    }
809
810    #[test]
811    fn test_token2022_extension_blocking_check() {
812        let config = ConfigBuilder::new()
813            .with_token2022_extensions(
814                vec!["transfer_fee_config", "pausable"],
815                vec!["memo_transfer"],
816            )
817            .build_config()
818            .unwrap();
819
820        // Test mint extension blocking
821        assert!(config
822            .validation
823            .token_2022
824            .is_mint_extension_blocked(ExtensionType::TransferFeeConfig));
825        assert!(config.validation.token_2022.is_mint_extension_blocked(ExtensionType::Pausable));
826        assert!(!config
827            .validation
828            .token_2022
829            .is_mint_extension_blocked(ExtensionType::NonTransferable));
830
831        // Test account extension blocking
832        assert!(config
833            .validation
834            .token_2022
835            .is_account_extension_blocked(ExtensionType::MemoTransfer));
836        assert!(!config
837            .validation
838            .token_2022
839            .is_account_extension_blocked(ExtensionType::CpiGuard));
840    }
841
842    #[test]
843    fn test_cache_config_parsing() {
844        let config = ConfigBuilder::new()
845            .with_cache_config(Some("redis://localhost:6379"), true, 600, 120)
846            .build_config()
847            .unwrap();
848
849        assert_eq!(config.kora.cache.url, Some("redis://localhost:6379".to_string()));
850        assert!(config.kora.cache.enabled);
851        assert_eq!(config.kora.cache.default_ttl, 600);
852        assert_eq!(config.kora.cache.account_ttl, 120);
853    }
854
855    #[test]
856    fn test_cache_config_default() {
857        let config = ConfigBuilder::new().build_config().unwrap();
858
859        assert_eq!(config.kora.cache.url, None);
860        assert!(!config.kora.cache.enabled);
861        assert_eq!(config.kora.cache.default_ttl, 300);
862        assert_eq!(config.kora.cache.account_ttl, 60);
863    }
864
865    #[test]
866    fn test_usage_limit_config_parsing() {
867        let config = ConfigBuilder::new()
868            .with_usage_limit_config(true, Some("redis://localhost:6379"), 10, false)
869            .build_config()
870            .unwrap();
871
872        assert!(config.kora.usage_limit.enabled);
873        assert_eq!(config.kora.usage_limit.cache_url, Some("redis://localhost:6379".to_string()));
874        assert_eq!(config.kora.usage_limit.max_transactions, 10);
875        assert!(!config.kora.usage_limit.fallback_if_unavailable);
876    }
877
878    #[test]
879    fn test_usage_limit_config_default() {
880        let config = ConfigBuilder::new().build_config().unwrap();
881
882        assert!(!config.kora.usage_limit.enabled);
883        assert_eq!(config.kora.usage_limit.cache_url, None);
884        assert_eq!(config.kora.usage_limit.max_transactions, DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS);
885        assert_eq!(
886            config.kora.usage_limit.fallback_if_unavailable,
887            DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE
888        );
889    }
890
891    #[test]
892    fn test_usage_limit_config_unlimited() {
893        let config = ConfigBuilder::new()
894            .with_usage_limit_config(true, None, 0, true)
895            .build_config()
896            .unwrap();
897
898        assert!(config.kora.usage_limit.enabled);
899        assert_eq!(config.kora.usage_limit.max_transactions, 0); // 0 = unlimited
900    }
901
902    #[test]
903    fn test_max_request_body_size_default() {
904        let config = ConfigBuilder::new().build_config().unwrap();
905
906        assert_eq!(config.kora.max_request_body_size, DEFAULT_MAX_REQUEST_BODY_SIZE);
907        assert_eq!(config.kora.max_request_body_size, 2 * 1024 * 1024); // 2 MB
908    }
909
910    #[test]
911    fn test_max_request_body_size_custom() {
912        let custom_size = 10 * 1024 * 1024; // 10 MB
913        let config =
914            ConfigBuilder::new().with_max_request_body_size(custom_size).build_config().unwrap();
915
916        assert_eq!(config.kora.max_request_body_size, custom_size);
917    }
918}