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
14pub 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 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 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 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 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 pub fn with_timeout(mut self, timeout: Duration) -> Self {
61 self.timeout = timeout;
62 self
63 }
64
65 pub fn with_custom_url(mut self, url: impl Into<String>) -> Self {
67 self.base_url = url.into();
68 self
69 }
70
71 pub fn with_paper_trading(mut self, paper_trading: bool) -> Self {
73 self.paper_trading = paper_trading;
74 self
75 }
76
77 pub fn paper_trading(mut self) -> Self {
79 self.paper_trading = true;
80 self
81 }
82
83 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 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 pub fn build(self) -> WebullResult<WebullClient> {
97 let device_id = self
99 .device_id
100 .unwrap_or_else(|| Uuid::new_v4().to_hyphenated().to_string());
101
102 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 let client = reqwest::Client::builder()
114 .timeout(config.timeout)
115 .build()
116 .map_err(|e| WebullError::NetworkError(e))?;
117
118 let token_store = self
120 .token_store
121 .unwrap_or_else(|| Box::new(MemoryTokenStore::default()));
122
123 let credential_store = self
125 .credential_store
126 .unwrap_or_else(|| Box::new(MemoryCredentialStore::default()));
127
128 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
144pub struct WebullClient {
146 inner: reqwest::Client,
148
149 config: WebullConfig,
151
152 auth_manager: Arc<AuthManager>,
154
155 credential_store: Arc<Box<dyn CredentialStore>>,
157}
158
159impl WebullClient {
160 pub fn builder() -> WebullClientBuilder {
162 WebullClientBuilder::new()
163 }
164
165 pub async fn login(&self, username: &str, password: &str) -> WebullResult<()> {
167 let mut auth_manager = AuthManager::new(
169 self.config.clone(),
170 Box::new(MemoryTokenStore::default()),
171 self.inner.clone(),
172 );
173
174 let token = auth_manager.authenticate(username, password).await?;
176
177 let token_store = self.auth_manager.token_store.as_ref();
179 token_store.store_token(token)?;
180
181 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 pub async fn logout(&self) -> WebullResult<()> {
193 let mut auth_manager = AuthManager::new(
195 self.config.clone(),
196 Box::new(MemoryTokenStore::default()),
197 self.inner.clone(),
198 );
199
200 let token = match self.auth_manager.token_store.get_token()? {
202 Some(token) => token,
203 None => {
204 return Ok(());
206 }
207 };
208
209 auth_manager.token_store.store_token(token)?;
211
212 auth_manager.revoke_token().await?;
214
215 self.auth_manager.token_store.clear_token()?;
217
218 self.credential_store.clear_credentials()?;
220
221 Ok(())
222 }
223
224 pub async fn refresh_token(&self) -> WebullResult<()> {
226 let mut auth_manager = AuthManager::new(
228 self.config.clone(),
229 Box::new(MemoryTokenStore::default()),
230 self.inner.clone(),
231 );
232
233 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 auth_manager.token_store.store_token(token)?;
245
246 let new_token = auth_manager.refresh_token().await?;
248
249 self.auth_manager.token_store.store_token(new_token)?;
251
252 Ok(())
253 }
254
255 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 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 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 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 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 pub fn get_credentials(&self) -> WebullResult<Option<crate::auth::Credentials>> {
299 self.credential_store.get_credentials()
300 }
301
302 pub fn credential_store(&self) -> &Arc<Box<dyn CredentialStore>> {
304 &self.credential_store
305 }
306
307 pub fn is_paper_trading(&self) -> bool {
309 self.config.paper_trading
310 }
311
312 pub fn paper_trading(&self) -> WebullResult<Self> {
314 let mut config = self.config.clone();
315 config.paper_trading = true;
316
317 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}