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