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
11pub 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 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 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 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 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 pub fn with_timeout(mut self, timeout: Duration) -> Self {
58 self.timeout = timeout;
59 self
60 }
61
62 pub fn with_custom_url(mut self, url: impl Into<String>) -> Self {
64 self.base_url = url.into();
65 self
66 }
67
68 pub fn with_paper_trading(mut self, paper_trading: bool) -> Self {
70 self.paper_trading = paper_trading;
71 self
72 }
73
74 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 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 pub fn build(self) -> WebullResult<WebullClient> {
88 let device_id = self.device_id.unwrap_or_else(|| Uuid::new_v4().to_hyphenated().to_string());
90
91 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 let client = reqwest::Client::builder()
103 .timeout(config.timeout)
104 .build()
105 .map_err(|e| WebullError::NetworkError(e))?;
106
107 let token_store = self.token_store.unwrap_or_else(|| Box::new(MemoryTokenStore::default()));
109
110 let credential_store = self.credential_store.unwrap_or_else(|| Box::new(MemoryCredentialStore::default()));
112
113 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
125pub struct WebullClient {
127 inner: reqwest::Client,
129
130 config: WebullConfig,
132
133 auth_manager: Arc<AuthManager>,
135
136 credential_store: Arc<Box<dyn CredentialStore>>,
138}
139
140impl WebullClient {
141 pub fn builder() -> WebullClientBuilder {
143 WebullClientBuilder::new()
144 }
145
146 pub async fn login(&self, username: &str, password: &str) -> WebullResult<()> {
148 let mut auth_manager = AuthManager::new(
150 self.config.clone(),
151 Box::new(MemoryTokenStore::default()),
152 self.inner.clone(),
153 );
154
155 let token = auth_manager.authenticate(username, password).await?;
157
158 let token_store = self.auth_manager.token_store.as_ref();
160 token_store.store_token(token)?;
161
162 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 pub async fn logout(&self) -> WebullResult<()> {
174 let mut auth_manager = AuthManager::new(
176 self.config.clone(),
177 Box::new(MemoryTokenStore::default()),
178 self.inner.clone(),
179 );
180
181 let token = match self.auth_manager.token_store.get_token()? {
183 Some(token) => token,
184 None => {
185 return Ok(());
187 }
188 };
189
190 auth_manager.token_store.store_token(token)?;
192
193 auth_manager.revoke_token().await?;
195
196 self.auth_manager.token_store.clear_token()?;
198
199 self.credential_store.clear_credentials()?;
201
202 Ok(())
203 }
204
205 pub async fn refresh_token(&self) -> WebullResult<()> {
207 let mut auth_manager = AuthManager::new(
209 self.config.clone(),
210 Box::new(MemoryTokenStore::default()),
211 self.inner.clone(),
212 );
213
214 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 auth_manager.token_store.store_token(token)?;
224
225 let new_token = auth_manager.refresh_token().await?;
227
228 self.auth_manager.token_store.store_token(new_token)?;
230
231 Ok(())
232 }
233
234 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 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 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 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 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 pub fn get_credentials(&self) -> WebullResult<Option<crate::auth::Credentials>> {
278 self.credential_store.get_credentials()
279 }
280
281 pub fn credential_store(&self) -> &Arc<Box<dyn CredentialStore>> {
283 &self.credential_store
284 }
285}