use crate::auth::{AuthManager, MemoryTokenStore, TokenStore};
use crate::config::WebullConfig;
use crate::endpoints::{account::AccountEndpoints, market_data::MarketDataEndpoints, orders::OrderEndpoints, watchlists::WatchlistEndpoints};
use crate::error::{WebullError, WebullResult};
use crate::streaming::client::WebSocketClient;
use crate::utils::credentials::{CredentialStore, MemoryCredentialStore};
use std::sync::Arc;
use std::time::Duration;
use uuid::Uuid;
pub struct WebullClientBuilder {
api_key: Option<String>,
api_secret: Option<String>,
device_id: Option<String>,
timeout: Duration,
base_url: String,
paper_trading: bool,
token_store: Option<Box<dyn TokenStore>>,
credential_store: Option<Box<dyn CredentialStore>>,
}
impl WebullClientBuilder {
pub fn new() -> Self {
Self {
api_key: None,
api_secret: None,
device_id: None,
timeout: Duration::from_secs(30),
base_url: "https://api.webull.com".to_string(),
paper_trading: false,
token_store: None,
credential_store: None,
}
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_api_secret(mut self, api_secret: impl Into<String>) -> Self {
self.api_secret = Some(api_secret.into());
self
}
pub fn with_device_id(mut self, device_id: impl Into<String>) -> Self {
self.device_id = Some(device_id.into());
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_custom_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn with_paper_trading(mut self, paper_trading: bool) -> Self {
self.paper_trading = paper_trading;
self
}
pub fn with_token_store(mut self, store: impl TokenStore + 'static) -> Self {
self.token_store = Some(Box::new(store));
self
}
pub fn with_credential_store(mut self, store: impl CredentialStore + 'static) -> Self {
self.credential_store = Some(Box::new(store));
self
}
pub fn build(self) -> WebullResult<WebullClient> {
let device_id = self.device_id.unwrap_or_else(|| Uuid::new_v4().to_hyphenated().to_string());
let config = WebullConfig {
api_key: self.api_key,
api_secret: self.api_secret,
device_id: Some(device_id),
timeout: self.timeout,
base_url: self.base_url,
paper_trading: self.paper_trading,
};
let client = reqwest::Client::builder()
.timeout(config.timeout)
.build()
.map_err(|e| WebullError::NetworkError(e))?;
let token_store = self.token_store.unwrap_or_else(|| Box::new(MemoryTokenStore::default()));
let credential_store = self.credential_store.unwrap_or_else(|| Box::new(MemoryCredentialStore::default()));
let auth_manager = Arc::new(AuthManager::new(config.clone(), token_store, client.clone()));
Ok(WebullClient {
inner: client,
config,
auth_manager,
credential_store: Arc::new(credential_store),
})
}
}
pub struct WebullClient {
inner: reqwest::Client,
config: WebullConfig,
auth_manager: Arc<AuthManager>,
credential_store: Arc<Box<dyn CredentialStore>>,
}
impl WebullClient {
pub fn builder() -> WebullClientBuilder {
WebullClientBuilder::new()
}
pub async fn login(&self, username: &str, password: &str) -> WebullResult<()> {
let mut auth_manager = AuthManager::new(
self.config.clone(),
Box::new(MemoryTokenStore::default()),
self.inner.clone(),
);
let token = auth_manager.authenticate(username, password).await?;
let token_store = self.auth_manager.token_store.as_ref();
token_store.store_token(token)?;
let credentials = crate::auth::Credentials {
username: username.to_string(),
password: password.to_string(),
};
self.credential_store.store_credentials(credentials)?;
Ok(())
}
pub async fn logout(&self) -> WebullResult<()> {
let mut auth_manager = AuthManager::new(
self.config.clone(),
Box::new(MemoryTokenStore::default()),
self.inner.clone(),
);
let token = match self.auth_manager.token_store.get_token()? {
Some(token) => token,
None => {
return Ok(());
}
};
auth_manager.token_store.store_token(token)?;
auth_manager.revoke_token().await?;
self.auth_manager.token_store.clear_token()?;
self.credential_store.clear_credentials()?;
Ok(())
}
pub async fn refresh_token(&self) -> WebullResult<()> {
let mut auth_manager = AuthManager::new(
self.config.clone(),
Box::new(MemoryTokenStore::default()),
self.inner.clone(),
);
let token = match self.auth_manager.token_store.get_token()? {
Some(token) => token,
None => {
return Err(WebullError::InvalidRequest("No token available for refresh".to_string()));
}
};
auth_manager.token_store.store_token(token)?;
let new_token = auth_manager.refresh_token().await?;
self.auth_manager.token_store.store_token(new_token)?;
Ok(())
}
pub fn accounts(&self) -> AccountEndpoints {
AccountEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
pub fn market_data(&self) -> MarketDataEndpoints {
MarketDataEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
pub fn orders(&self) -> OrderEndpoints {
OrderEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
pub fn watchlists(&self) -> WatchlistEndpoints {
WatchlistEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
pub fn streaming(&self) -> WebSocketClient {
let ws_base_url = self.config.base_url.clone().replace("http", "ws");
WebSocketClient::new(ws_base_url, self.auth_manager.clone())
}
pub fn get_credentials(&self) -> WebullResult<Option<crate::auth::Credentials>> {
self.credential_store.get_credentials()
}
pub fn credential_store(&self) -> &Arc<Box<dyn CredentialStore>> {
&self.credential_store
}
}