webull_rs/
client.rs

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