Skip to main content

truefix_ig_client/
config.rs

1use std::time::Duration;
2
3use crate::error::{IgError, IgResult};
4
5/// Credentials bound to one IG identity.
6#[derive(Clone, PartialEq, Eq)]
7pub struct Credentials {
8    identifier: String,
9    password: String,
10    api_key: String,
11}
12
13impl Credentials {
14    /// Creates credentials after rejecting empty values.
15    pub fn new(
16        identifier: impl Into<String>,
17        password: impl Into<String>,
18        api_key: impl Into<String>,
19    ) -> IgResult<Self> {
20        let credentials = Self {
21            identifier: identifier.into(),
22            password: password.into(),
23            api_key: api_key.into(),
24        };
25        if credentials.identifier.is_empty()
26            || credentials.password.is_empty()
27            || credentials.api_key.is_empty()
28        {
29            return Err(IgError::InvalidConfiguration(
30                "identifier, password, and API key must be non-empty".to_owned(),
31            ));
32        }
33        Ok(credentials)
34    }
35
36    pub(crate) fn identifier(&self) -> &str {
37        &self.identifier
38    }
39    pub(crate) fn password(&self) -> &str {
40        &self.password
41    }
42    pub(crate) fn api_key(&self) -> &str {
43        &self.api_key
44    }
45}
46
47impl std::fmt::Debug for Credentials {
48    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        formatter.write_str("Credentials(REDACTED)")
50    }
51}
52
53/// Marker required to select IG's live environment.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct LiveTradingConfirmation(());
56
57impl LiveTradingConfirmation {
58    /// Explicitly acknowledges that this client may submit live trading requests.
59    pub const fn acknowledge_risk() -> Self {
60        Self(())
61    }
62}
63
64/// Endpoint environment selected when the client is constructed.
65#[derive(Debug, Clone, PartialEq, Eq, Default)]
66pub enum Environment {
67    /// IG demo dealing API, suitable for simulated trading.
68    #[default]
69    Demo,
70    /// IG production dealing API, permitted only with explicit confirmation.
71    Live(LiveTradingConfirmation),
72    /// Caller-controlled endpoint, primarily for tests or supported regional routing.
73    Custom { rest_base: String },
74}
75
76impl Environment {
77    const DEMO_REST_BASE: &str = "https://demo-api.ig.com/gateway/deal";
78    const LIVE_REST_BASE: &str = "https://api.ig.com/gateway/deal";
79
80    /// Returns the dealing API base URL.
81    pub fn rest_base(&self) -> &str {
82        match self {
83            Self::Demo => Self::DEMO_REST_BASE,
84            Self::Live(_) => Self::LIVE_REST_BASE,
85            Self::Custom { rest_base } => rest_base,
86        }
87    }
88}
89
90/// Session authentication protocol used for REST requests.
91#[derive(Debug, Clone, PartialEq, Eq, Default)]
92pub enum AuthenticationVersion {
93    /// IG's CST and X-SECURITY-TOKEN session authentication.
94    #[default]
95    V2,
96    /// OAuth authentication. IG requires the active account to be selected at login.
97    V3 { account_id: String },
98}
99
100/// Fully immutable client configuration.
101#[derive(Debug, Clone)]
102pub struct ClientConfig {
103    pub environment: Environment,
104    pub credentials: Option<Credentials>,
105    pub authentication: AuthenticationVersion,
106    pub timeout: Duration,
107    pub proxy: Option<String>,
108}
109
110impl Default for ClientConfig {
111    fn default() -> Self {
112        Self {
113            environment: Environment::Demo,
114            credentials: None,
115            authentication: AuthenticationVersion::V2,
116            timeout: Duration::from_secs(15),
117            proxy: None,
118        }
119    }
120}
121
122impl ClientConfig {
123    /// Returns a demo configuration with optional credentials.
124    pub fn demo(credentials: Option<Credentials>) -> Self {
125        Self {
126            credentials,
127            ..Self::default()
128        }
129    }
130
131    /// Creates an explicitly confirmed live configuration.
132    pub fn live(credentials: Credentials, confirmation: LiveTradingConfirmation) -> Self {
133        Self {
134            environment: Environment::Live(confirmation),
135            credentials: Some(credentials),
136            ..Self::default()
137        }
138    }
139
140    /// Selects OAuth v3 authentication for the supplied IG account.
141    pub fn with_v3_authentication(mut self, account_id: impl Into<String>) -> IgResult<Self> {
142        let account_id = account_id.into();
143        if account_id.is_empty() {
144            return Err(IgError::InvalidConfiguration(
145                "v3 authentication requires a non-empty account ID".to_owned(),
146            ));
147        }
148        self.authentication = AuthenticationVersion::V3 { account_id };
149        Ok(self)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn credentials_redact_debug_output() {
159        let credentials = Credentials::new("user", "secret", "key").unwrap();
160        assert!(!format!("{credentials:?}").contains("secret"));
161    }
162
163    #[test]
164    fn demo_is_default() {
165        assert_eq!(ClientConfig::default().environment, Environment::Demo);
166    }
167
168    #[test]
169    fn v3_requires_an_account_id() {
170        assert!(ClientConfig::default().with_v3_authentication("").is_err());
171        assert_eq!(
172            ClientConfig::default()
173                .with_v3_authentication("ABC123")
174                .unwrap()
175                .authentication,
176            AuthenticationVersion::V3 {
177                account_id: "ABC123".to_owned(),
178            }
179        );
180    }
181}