1#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
21use nautilus_model::{enums::AccountType, identifiers::AccountId};
22use nautilus_network::websocket::TransportBackend;
23use serde::{Deserialize, Serialize};
24
25use crate::common::{
26 enums::{CoinbaseEnvironment, CoinbaseMarginType},
27 urls,
28};
29
30#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
32#[serde(default, deny_unknown_fields)]
33#[cfg_attr(
34 feature = "python",
35 pyo3::pyclass(module = "nautilus_trader.adapters.coinbase", from_py_object)
36)]
37#[cfg_attr(
38 feature = "python",
39 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.coinbase")
40)]
41pub struct CoinbaseDataClientConfig {
42 pub api_key: Option<SecretString>,
44 pub api_secret: Option<SecretString>,
46 #[builder(default)]
48 pub environment: CoinbaseEnvironment,
49 pub base_url_rest: Option<String>,
51 pub base_url_ws: Option<String>,
53 pub proxy_url: Option<SecretString>,
55 #[builder(default = 10)]
57 pub http_timeout_secs: u64,
58 #[builder(default = 30)]
60 pub ws_timeout_secs: u64,
61 #[builder(default = 60)]
63 pub update_instruments_interval_mins: u64,
64 #[builder(default = 15)]
69 pub derivatives_poll_interval_secs: u64,
70 #[builder(default)]
72 pub transport_backend: TransportBackend,
73}
74
75#[cfg(feature = "python")]
76nautilus_core::impl_pyo3_config_getters!(CoinbaseDataClientConfig {
77 environment: CoinbaseEnvironment,
78 base_url_rest: Option<String>,
79 base_url_ws: Option<String>,
80 http_timeout_secs: u64,
81 ws_timeout_secs: u64,
82 update_instruments_interval_mins: u64,
83 derivatives_poll_interval_secs: u64,
84 transport_backend: TransportBackend,
85});
86
87impl Default for CoinbaseDataClientConfig {
88 fn default() -> Self {
89 Self::builder().build()
90 }
91}
92
93impl CoinbaseDataClientConfig {
94 #[must_use]
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 #[must_use]
102 pub fn has_credentials(&self) -> bool {
103 self.api_key
104 .as_ref()
105 .map(SecretString::expose_secret)
106 .is_some_and(|s| !s.trim().is_empty())
107 && self
108 .api_secret
109 .as_ref()
110 .map(SecretString::expose_secret)
111 .is_some_and(|s| !s.trim().is_empty())
112 }
113
114 #[must_use]
116 pub fn rest_url(&self) -> String {
117 self.base_url_rest
118 .clone()
119 .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
120 }
121
122 #[must_use]
124 pub fn ws_url(&self) -> String {
125 self.base_url_ws
126 .clone()
127 .unwrap_or_else(|| urls::ws_url(self.environment).to_string())
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
133#[serde(default, deny_unknown_fields)]
134#[cfg_attr(
135 feature = "python",
136 pyo3::pyclass(module = "nautilus_trader.adapters.coinbase", from_py_object)
137)]
138#[cfg_attr(
139 feature = "python",
140 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.coinbase")
141)]
142pub struct CoinbaseExecutionClientConfig {
143 #[builder(default = AccountId::from("COINBASE-001"))]
145 pub account_id: AccountId,
146 pub api_key: Option<SecretString>,
148 pub api_secret: Option<SecretString>,
150 #[builder(default)]
152 pub environment: CoinbaseEnvironment,
153 pub base_url_rest: Option<String>,
155 pub base_url_ws: Option<String>,
157 pub proxy_url: Option<SecretString>,
159 #[builder(default = 10)]
161 pub http_timeout_secs: u64,
162 #[builder(default = 3)]
164 pub max_retries: u32,
165 #[builder(default = 100)]
167 pub retry_delay_initial_ms: u64,
168 #[builder(default = 5000)]
170 pub retry_delay_max_ms: u64,
171 #[builder(default = AccountType::Cash)]
174 pub account_type: AccountType,
175 pub default_margin_type: Option<CoinbaseMarginType>,
178 pub default_leverage: Option<rust_decimal::Decimal>,
181 pub retail_portfolio_id: Option<String>,
186 #[builder(default)]
188 pub transport_backend: TransportBackend,
189}
190
191#[cfg(feature = "python")]
192nautilus_core::impl_pyo3_config_getters!(CoinbaseExecutionClientConfig {
193 account_id: AccountId,
194 environment: CoinbaseEnvironment,
195 base_url_rest: Option<String>,
196 base_url_ws: Option<String>,
197 http_timeout_secs: u64,
198 max_retries: u32,
199 retry_delay_initial_ms: u64,
200 retry_delay_max_ms: u64,
201 account_type: AccountType,
202 default_margin_type: Option<CoinbaseMarginType>,
203 default_leverage: Option<rust_decimal::Decimal>,
204 retail_portfolio_id: Option<String>,
205 transport_backend: TransportBackend,
206});
207
208impl Default for CoinbaseExecutionClientConfig {
209 fn default() -> Self {
210 Self::builder().build()
211 }
212}
213
214impl CoinbaseExecutionClientConfig {
215 #[must_use]
217 pub fn new() -> Self {
218 Self::default()
219 }
220
221 #[must_use]
223 pub fn has_credentials(&self) -> bool {
224 self.api_key
225 .as_ref()
226 .map(SecretString::expose_secret)
227 .is_some_and(|s| !s.trim().is_empty())
228 && self
229 .api_secret
230 .as_ref()
231 .map(SecretString::expose_secret)
232 .is_some_and(|s| !s.trim().is_empty())
233 }
234
235 #[must_use]
237 pub fn rest_url(&self) -> String {
238 self.base_url_rest
239 .clone()
240 .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
241 }
242
243 #[must_use]
245 pub fn ws_url(&self) -> String {
246 self.base_url_ws
247 .clone()
248 .unwrap_or_else(|| urls::ws_user_url(self.environment).to_string())
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use rstest::rstest;
255
256 use super::*;
257
258 #[rstest]
259 fn test_config_debug_redacts_credentials() {
260 let data = CoinbaseDataClientConfig {
261 api_key: Some("data-api-key".into()),
262 api_secret: Some("data-api-secret".into()),
263 proxy_url: Some("http://user:data-proxy@localhost".into()),
264 ..Default::default()
265 };
266 let execution = CoinbaseExecutionClientConfig {
267 api_key: Some("exec-api-key".into()),
268 api_secret: Some("exec-api-secret".into()),
269 proxy_url: Some("http://user:exec-proxy@localhost".into()),
270 ..Default::default()
271 };
272
273 let formatted = format!("{data:?} {execution:?}");
274
275 assert_eq!(formatted.matches(REDACTED).count(), 6);
276
277 for secret in [
278 "data-api-key",
279 "data-api-secret",
280 "data-proxy",
281 "exec-api-key",
282 "exec-api-secret",
283 "exec-proxy",
284 ] {
285 assert!(!formatted.contains(secret));
286 }
287 }
288
289 #[rstest]
290 fn test_data_config_defaults() {
291 let config = CoinbaseDataClientConfig::default();
292 assert_eq!(config.environment, CoinbaseEnvironment::Live);
293 assert_eq!(config.http_timeout_secs, 10);
294 assert_eq!(config.ws_timeout_secs, 30);
295 assert_eq!(config.update_instruments_interval_mins, 60);
296 assert!(!config.has_credentials());
297 }
298
299 #[rstest]
300 fn test_data_config_has_credentials() {
301 let config = CoinbaseDataClientConfig {
302 api_key: Some("key".into()),
303 api_secret: Some("secret".into()),
304 ..CoinbaseDataClientConfig::default()
305 };
306 assert!(config.has_credentials());
307 }
308
309 #[rstest]
310 fn test_data_config_empty_credentials() {
311 let config = CoinbaseDataClientConfig {
312 api_key: Some(" ".into()),
313 api_secret: Some("secret".into()),
314 ..CoinbaseDataClientConfig::default()
315 };
316 assert!(!config.has_credentials());
317 }
318
319 #[rstest]
320 fn test_data_config_urls_live() {
321 let config = CoinbaseDataClientConfig::default();
322 assert!(config.rest_url().contains("api.coinbase.com"));
323 assert!(config.ws_url().contains("advanced-trade-ws.coinbase.com"));
324 }
325
326 #[rstest]
327 fn test_data_config_urls_sandbox() {
328 let config = CoinbaseDataClientConfig {
329 environment: CoinbaseEnvironment::Sandbox,
330 ..CoinbaseDataClientConfig::default()
331 };
332 assert!(config.rest_url().contains("sandbox"));
333 assert!(config.ws_url().contains("sandbox"));
334 }
335
336 #[rstest]
337 fn test_exec_config_defaults() {
338 let config = CoinbaseExecutionClientConfig::default();
339 assert_eq!(config.environment, CoinbaseEnvironment::Live);
340 assert_eq!(config.http_timeout_secs, 10);
341 assert_eq!(config.max_retries, 3);
342 }
343
344 #[rstest]
345 fn test_exec_config_ws_url_uses_user_endpoint() {
346 let config = CoinbaseExecutionClientConfig::default();
347 assert!(config.ws_url().contains("user"));
348 }
349
350 #[rstest]
351 fn test_data_config_toml_minimal() {
352 let config: CoinbaseDataClientConfig = toml::from_str(
353 r#"
354environment = "Sandbox"
355http_timeout_secs = 5
356update_instruments_interval_mins = 30
357derivatives_poll_interval_secs = 60
358"#,
359 )
360 .unwrap();
361
362 assert_eq!(config.environment, CoinbaseEnvironment::Sandbox);
363 assert_eq!(config.http_timeout_secs, 5);
364 assert_eq!(config.update_instruments_interval_mins, 30);
365 assert_eq!(config.derivatives_poll_interval_secs, 60);
366 }
367
368 #[rstest]
369 fn test_exec_config_toml_empty_uses_defaults() {
370 let config: CoinbaseExecutionClientConfig = toml::from_str("").unwrap();
371 let expected = CoinbaseExecutionClientConfig::default();
372
373 assert_eq!(config.environment, expected.environment);
374 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
375 assert_eq!(config.max_retries, expected.max_retries);
376 assert_eq!(config.account_type, expected.account_type);
377 assert_eq!(config.transport_backend, expected.transport_backend);
378 }
379}