webull_rs/
client.rs

1use crate::auth::{AuthManager, MemoryTokenStore, TokenStore};
2use crate::config::WebullConfig;
3use crate::endpoints::{
4    account::AccountEndpoints, market_data::MarketDataEndpoints, orders::OrderEndpoints,
5    watchlists::WatchlistEndpoints,
6};
7use crate::error::{WebullError, WebullResult};
8use crate::streaming::client::WebSocketClient;
9use crate::utils::credentials::{CredentialStore, MemoryCredentialStore};
10use std::sync::Arc;
11use std::time::Duration;
12use uuid::Uuid;
13
14/// Builder for creating a WebullClient.
15pub struct WebullClientBuilder {
16    api_key: Option<String>,
17    api_secret: Option<String>,
18    device_id: Option<String>,
19    timeout: Duration,
20    base_url: String,
21    paper_trading: bool,
22    token_store: Option<Box<dyn TokenStore>>,
23    credential_store: Option<Box<dyn CredentialStore>>,
24}
25
26impl WebullClientBuilder {
27    /// Create a new builder with default values.
28    pub fn new() -> Self {
29        Self {
30            api_key: None,
31            api_secret: None,
32            device_id: None,
33            timeout: Duration::from_secs(30),
34            base_url: "https://api.webull.com".to_string(),
35            paper_trading: false,
36            token_store: None,
37            credential_store: None,
38        }
39    }
40
41    /// Set the API key.
42    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
43        self.api_key = Some(api_key.into());
44        self
45    }
46
47    /// Set the API secret.
48    pub fn with_api_secret(mut self, api_secret: impl Into<String>) -> Self {
49        self.api_secret = Some(api_secret.into());
50        self
51    }
52
53    /// Set the device ID.
54    pub fn with_device_id(mut self, device_id: impl Into<String>) -> Self {
55        self.device_id = Some(device_id.into());
56        self
57    }
58
59    /// Set the timeout for API requests.
60    pub fn with_timeout(mut self, timeout: Duration) -> Self {
61        self.timeout = timeout;
62        self
63    }
64
65    /// Set a custom base URL.
66    pub fn with_custom_url(mut self, url: impl Into<String>) -> Self {
67        self.base_url = url.into();
68        self
69    }
70
71    /// Set whether to use paper trading.
72    pub fn with_paper_trading(mut self, paper_trading: bool) -> Self {
73        self.paper_trading = paper_trading;
74        self
75    }
76
77    /// Enable paper trading.
78    pub fn paper_trading(mut self) -> Self {
79        self.paper_trading = true;
80        self
81    }
82
83    /// Set a custom token store.
84    pub fn with_token_store(mut self, store: impl TokenStore + 'static) -> Self {
85        self.token_store = Some(Box::new(store));
86        self
87    }
88
89    /// Set a custom credential store.
90    pub fn with_credential_store(mut self, store: impl CredentialStore + 'static) -> Self {
91        self.credential_store = Some(Box::new(store));
92        self
93    }
94
95    /// Build the WebullClient.
96    pub fn build(self) -> WebullResult<WebullClient> {
97        // Generate a random device ID if not provided
98        let device_id = self
99            .device_id
100            .unwrap_or_else(|| Uuid::new_v4().to_hyphenated().to_string());
101
102        // Create the configuration
103        let config = WebullConfig {
104            api_key: self.api_key,
105            api_secret: self.api_secret,
106            device_id: Some(device_id),
107            timeout: self.timeout,
108            base_url: self.base_url,
109            paper_trading: self.paper_trading,
110        };
111
112        // Create the HTTP client
113        let client = reqwest::Client::builder()
114            .timeout(config.timeout)
115            .build()
116            .map_err(|e| WebullError::NetworkError(e))?;
117
118        // Create the token store
119        let token_store = self
120            .token_store
121            .unwrap_or_else(|| Box::new(MemoryTokenStore::default()));
122
123        // Create the credential store
124        let credential_store = self
125            .credential_store
126            .unwrap_or_else(|| Box::new(MemoryCredentialStore::default()));
127
128        // Create the auth manager
129        let auth_manager = Arc::new(AuthManager::new(
130            config.clone(),
131            token_store,
132            client.clone(),
133        ));
134
135        Ok(WebullClient {
136            inner: client,
137            config,
138            auth_manager,
139            credential_store: Arc::new(credential_store),
140        })
141    }
142}
143
144/// Client for interacting with the Webull API.
145pub struct WebullClient {
146    /// HTTP client
147    inner: reqwest::Client,
148
149    /// Configuration
150    config: WebullConfig,
151
152    /// Authentication manager
153    auth_manager: Arc<AuthManager>,
154
155    /// Credential store
156    credential_store: Arc<Box<dyn CredentialStore>>,
157}
158
159impl WebullClient {
160    /// Create a new builder for configuring the client.
161    pub fn builder() -> WebullClientBuilder {
162        WebullClientBuilder::new()
163    }
164
165    /// Login to Webull.
166    pub async fn login(&self, username: &str, password: &str) -> WebullResult<()> {
167        // Create a new AuthManager with the same configuration
168        let mut auth_manager = AuthManager::new(
169            self.config.clone(),
170            Box::new(MemoryTokenStore::default()),
171            self.inner.clone(),
172        );
173
174        // Authenticate
175        let token = auth_manager.authenticate(username, password).await?;
176
177        // Store the token in the original auth_manager
178        let token_store = self.auth_manager.token_store.as_ref();
179        token_store.store_token(token)?;
180
181        // Store the credentials
182        let credentials = crate::auth::Credentials {
183            username: username.to_string(),
184            password: password.to_string(),
185        };
186        self.credential_store.store_credentials(credentials)?;
187
188        Ok(())
189    }
190
191    /// Logout from Webull.
192    pub async fn logout(&self) -> WebullResult<()> {
193        // Create a new AuthManager with the same configuration
194        let mut auth_manager = AuthManager::new(
195            self.config.clone(),
196            Box::new(MemoryTokenStore::default()),
197            self.inner.clone(),
198        );
199
200        // Get the current token from the original auth_manager
201        let token = match self.auth_manager.token_store.get_token()? {
202            Some(token) => token,
203            None => {
204                // No token to revoke
205                return Ok(());
206            }
207        };
208
209        // Store the token in the new auth_manager
210        auth_manager.token_store.store_token(token)?;
211
212        // Revoke the token
213        auth_manager.revoke_token().await?;
214
215        // Clear the token in the original auth_manager
216        self.auth_manager.token_store.clear_token()?;
217
218        // Clear the credentials
219        self.credential_store.clear_credentials()?;
220
221        Ok(())
222    }
223
224    /// Refresh the authentication token.
225    pub async fn refresh_token(&self) -> WebullResult<()> {
226        // Create a new AuthManager with the same configuration
227        let mut auth_manager = AuthManager::new(
228            self.config.clone(),
229            Box::new(MemoryTokenStore::default()),
230            self.inner.clone(),
231        );
232
233        // Get the current token from the original auth_manager
234        let token = match self.auth_manager.token_store.get_token()? {
235            Some(token) => token,
236            None => {
237                return Err(WebullError::InvalidRequest(
238                    "No token available for refresh".to_string(),
239                ));
240            }
241        };
242
243        // Store the token in the new auth_manager
244        auth_manager.token_store.store_token(token)?;
245
246        // Refresh the token
247        let new_token = auth_manager.refresh_token().await?;
248
249        // Store the new token in the original auth_manager
250        self.auth_manager.token_store.store_token(new_token)?;
251
252        Ok(())
253    }
254
255    /// Get account endpoints.
256    pub fn accounts(&self) -> AccountEndpoints {
257        AccountEndpoints::new(
258            self.inner.clone(),
259            self.config.base_url.clone(),
260            self.auth_manager.clone(),
261        )
262    }
263
264    /// Get market data endpoints.
265    pub fn market_data(&self) -> MarketDataEndpoints {
266        MarketDataEndpoints::new(
267            self.inner.clone(),
268            self.config.base_url.clone(),
269            self.auth_manager.clone(),
270        )
271    }
272
273    /// Get order endpoints.
274    pub fn orders(&self) -> OrderEndpoints {
275        OrderEndpoints::new(
276            self.inner.clone(),
277            self.config.base_url.clone(),
278            self.auth_manager.clone(),
279        )
280    }
281
282    /// Get watchlist endpoints.
283    pub fn watchlists(&self) -> WatchlistEndpoints {
284        WatchlistEndpoints::new(
285            self.inner.clone(),
286            self.config.base_url.clone(),
287            self.auth_manager.clone(),
288        )
289    }
290
291    /// Create a WebSocket client for streaming data.
292    pub fn streaming(&self) -> WebSocketClient {
293        let ws_base_url = self.config.base_url.clone().replace("http", "ws");
294        WebSocketClient::new(ws_base_url, self.auth_manager.clone())
295    }
296
297    /// Get the stored credentials.
298    pub fn get_credentials(&self) -> WebullResult<Option<crate::auth::Credentials>> {
299        self.credential_store.get_credentials()
300    }
301
302    /// Get the credential store.
303    pub fn credential_store(&self) -> &Arc<Box<dyn CredentialStore>> {
304        &self.credential_store
305    }
306
307    /// Check if the client is configured for paper trading.
308    pub fn is_paper_trading(&self) -> bool {
309        self.config.paper_trading
310    }
311
312    /// Create a new client for paper trading.
313    pub fn paper_trading(&self) -> WebullResult<Self> {
314        let mut config = self.config.clone();
315        config.paper_trading = true;
316
317        // Create a new client with the same settings but for paper trading
318        let client = reqwest::ClientBuilder::new()
319            .timeout(config.timeout)
320            .build()
321            .map_err(|e| WebullError::NetworkError(e))?;
322
323        let token_store = Box::new(MemoryTokenStore::default());
324        let credential_store = Box::new(MemoryCredentialStore::default());
325
326        let auth_manager = Arc::new(AuthManager::new(
327            config.clone(),
328            token_store,
329            client.clone(),
330        ));
331
332        Ok(Self {
333            inner: client,
334            config,
335            auth_manager,
336            credential_store: Arc::new(credential_store),
337        })
338    }
339}