use std::time::Duration;
use crate::api::balances::BalancesApi;
use crate::api::configuration::ConfigurationApi;
use crate::api::market_data::MarketDataApi;
use crate::api::orders::OrdersApi;
use crate::api::trades::TradesApi;
use crate::auth::Credentials;
use crate::error::{Error, Result};
use crate::transport::Transport;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Environment {
Production,
Dev,
}
impl Environment {
pub const fn base_url(self) -> &'static str {
match self {
Self::Production => "https://revx.revolut.com/api/1.0",
Self::Dev => "https://revx.revolut.codes/api/1.0",
}
}
}
#[derive(Debug, Clone)]
pub struct RevolutXClient {
transport: Transport,
}
impl RevolutXClient {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn base_url(&self) -> &str {
self.transport.base_url()
}
pub fn is_authenticated(&self) -> bool {
self.transport.has_credentials()
}
pub fn balances(&self) -> BalancesApi<'_> {
BalancesApi::new(self)
}
pub fn configuration(&self) -> ConfigurationApi<'_> {
ConfigurationApi::new(self)
}
pub fn market_data(&self) -> MarketDataApi<'_> {
MarketDataApi::new(self)
}
pub fn orders(&self) -> OrdersApi<'_> {
OrdersApi::new(self)
}
pub fn trades(&self) -> TradesApi<'_> {
TradesApi::new(self)
}
pub(crate) fn transport(&self) -> &Transport {
&self.transport
}
}
#[derive(Clone)]
enum KeySource {
Pem(String),
Seed([u8; 32]),
}
#[derive(Default)]
pub struct ClientBuilder {
api_key: Option<String>,
key: Option<KeySource>,
environment: Option<Environment>,
base_url: Option<String>,
timeout: Option<Duration>,
http_client: Option<reqwest::Client>,
}
impl ClientBuilder {
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn private_key_pem(mut self, pem: impl Into<String>) -> Self {
self.key = Some(KeySource::Pem(pem.into()));
self
}
pub fn private_key_bytes(mut self, seed: [u8; 32]) -> Self {
self.key = Some(KeySource::Seed(seed));
self
}
pub fn environment(mut self, environment: Environment) -> Self {
self.environment = Some(environment);
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http_client = Some(client);
self
}
pub fn selected_environment(&self) -> Environment {
self.environment.unwrap_or(Environment::Production)
}
pub fn build(self) -> Result<RevolutXClient> {
let default_base_url = self.selected_environment().base_url().to_owned();
let base_url = self.base_url.unwrap_or(default_base_url);
if base_url.trim().is_empty() {
return Err(Error::configuration("base URL must not be empty"));
}
let credentials = match (self.api_key, self.key) {
(Some(api_key), Some(KeySource::Pem(pem))) => {
Some(Credentials::from_pem(api_key, &pem)?)
}
(Some(api_key), Some(KeySource::Seed(seed))) => {
Some(Credentials::from_seed(api_key, seed))
}
(None, None) => None,
(Some(_), None) => {
return Err(Error::configuration(
"an API key was provided but no private key; call private_key_pem or private_key_bytes",
));
}
(None, Some(_)) => {
return Err(Error::configuration(
"a private key was provided but no API key; call api_key",
));
}
};
let http_client = match self.http_client {
Some(client) => client,
None => reqwest::Client::builder()
.timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT))
.build()
.map_err(|e| Error::configuration(format!("could not build HTTP client: {e}")))?,
};
let transport = Transport::new(&base_url, http_client, credentials)?;
Ok(RevolutXClient { transport })
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_SEED: [u8; 32] = [7u8; 32];
#[test]
fn default_environment_is_production() {
let client = RevolutXClient::builder().build().unwrap();
assert_eq!(client.base_url(), Environment::Production.base_url());
assert!(!client.is_authenticated());
}
#[test]
fn dev_environment_uses_dev_base_url() {
let client = RevolutXClient::builder()
.environment(Environment::Dev)
.build()
.unwrap();
assert_eq!(client.base_url(), Environment::Dev.base_url());
}
#[test]
fn custom_base_url_overrides_environment() {
let client = RevolutXClient::builder()
.environment(Environment::Dev)
.base_url("http://127.0.0.1:8080/api/1.0")
.build()
.unwrap();
assert_eq!(client.base_url(), "http://127.0.0.1:8080/api/1.0");
}
#[test]
fn credentials_make_client_authenticated() {
let client = RevolutXClient::builder()
.api_key("key")
.private_key_bytes(TEST_SEED)
.build()
.unwrap();
assert!(client.is_authenticated());
}
#[test]
fn partial_credentials_are_rejected() {
let err = RevolutXClient::builder()
.api_key("key")
.build()
.unwrap_err();
assert!(matches!(err, Error::Configuration { .. }));
let err = RevolutXClient::builder()
.private_key_bytes(TEST_SEED)
.build()
.unwrap_err();
assert!(matches!(err, Error::Configuration { .. }));
}
#[test]
fn empty_custom_base_url_is_rejected() {
let err = RevolutXClient::builder()
.base_url(" ")
.build()
.unwrap_err();
assert!(matches!(err, Error::Configuration { .. }));
}
#[test]
fn invalid_pem_surfaces_key_error() {
let err = RevolutXClient::builder()
.api_key("key")
.private_key_pem("not a pem")
.build()
.unwrap_err();
assert!(matches!(err, Error::Key { .. }));
}
}