use std::future::Future;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use crate::client::http::transport::wire::{Method, TransportError};
pub const API_PREFIX: &str = "/api/v1/";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Call {
pub method: Method,
pub path: &'static str,
pub fields: Vec<(&'static str, String)>,
pub auth: bool,
pub json: Option<String>,
}
impl Call {
#[must_use]
pub const fn get(path: &'static str) -> Call {
Call { method: Method::Get, path, fields: Vec::new(), auth: false, json: None }
}
#[must_use]
pub const fn post(path: &'static str) -> Call {
Call { method: Method::Post, path, fields: Vec::new(), auth: false, json: None }
}
#[must_use]
pub fn field(mut self, key: &'static str, value: impl ToString) -> Call {
self.fields.push((key, value.to_string()));
self
}
#[must_use]
pub fn maybe(self, key: &'static str, value: Option<impl ToString>) -> Call {
match value {
Some(value) => self.field(key, value),
None => self,
}
}
#[must_use]
pub fn with_json(mut self, body: String) -> Call {
self.json = Some(body);
self
}
#[must_use]
pub const fn authed(mut self) -> Call {
self.auth = true;
self
}
#[must_use]
pub fn url_path(&self) -> String {
match self.path.starts_with('/') {
true => self.path.to_owned(),
false => format!("{API_PREFIX}{}", self.path),
}
}
#[must_use]
pub fn value(&self, key: &str) -> Option<&str> {
self.fields.iter().find(|(name, _)| *name == key).map(|(_, value)| value.as_str())
}
}
pub trait Transport {
fn call(&self, call: Call) -> impl Future<Output = Result<String, TransportError>>;
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ApiError {
#[error(transparent)]
Transport(#[from] TransportError),
#[error("the venue's answer on {path} could not be read: {detail}")]
Unreadable { path: String, detail: String },
#[error("refused ({code}): {message}")]
Refused { code: i64, message: String },
}
pub mod codes {
pub const OK: i64 = 200;
pub const INVALID_PARAM: i64 = 20001;
pub const INVALID_AUTH: i64 = 20013;
pub const ACCOUNT_NOT_FOUND: i64 = 21100;
pub const INVALID_NONCE: i64 = 21104;
pub const NON_INCREASING_NONCE: i64 = 21105;
pub const INVALID_PUBLIC_KEY: i64 = 21108;
pub const API_KEY_NOT_FOUND: i64 = 21109;
pub const INVALID_SIGNATURE: i64 = 21120;
pub const ZERO_COLLATERAL_CHANGE_PUB_KEY: i64 = 21126;
pub const INVALID_FEE: i64 = 21134;
pub const PUBLIC_KEY_UPDATE_SDK: i64 = 21136;
pub const TOO_MANY_PENDING: i64 = 21506;
pub const BELOW_MAINTENANCE_MARGIN: i64 = 21507;
pub const BELOW_INITIAL_MARGIN: i64 = 21508;
pub const NOT_ENOUGH_COLLATERAL: i64 = 21301;
pub const NOT_ENOUGH_ORDER_MARGIN: i64 = 21739;
pub const CLIENT_ORDER_INDEX_EXISTS: i64 = 21728;
pub const TOO_MANY_REQUESTS: i64 = 23000;
pub const TOO_MANY_FAST_WITHDRAWALS: i64 = 23005;
pub const INTERNAL: i64 = 29500;
pub const NOT_FOUND: i64 = 29404;
}
impl ApiError {
#[must_use]
pub const fn code(&self) -> Option<i64> {
match self {
ApiError::Refused { code, .. } => Some(*code),
_ => None,
}
}
#[must_use]
pub const fn is_rate_limited(&self) -> bool {
matches!(self, ApiError::Transport(TransportError::RateLimited { .. }))
|| matches!(self.code(), Some(codes::TOO_MANY_REQUESTS | codes::TOO_MANY_PENDING))
}
#[must_use]
pub const fn is_nonce(&self) -> bool {
matches!(self.code(), Some(codes::INVALID_NONCE | codes::NON_INCREASING_NONCE))
}
#[must_use]
pub const fn is_unregistered_key(&self) -> bool {
matches!(
self.code(),
Some(
codes::INVALID_PUBLIC_KEY
| codes::API_KEY_NOT_FOUND
| codes::INVALID_SIGNATURE
| codes::PUBLIC_KEY_UPDATE_SDK
)
)
}
#[must_use]
pub const fn may_have_landed(&self) -> bool {
match self {
ApiError::Transport(TransportError::Unreachable(_)) | ApiError::Unreadable { .. } => true,
ApiError::Transport(TransportError::Status { status, .. }) => *status >= 500,
ApiError::Transport(_) => false,
ApiError::Refused { code, .. } => *code == codes::INTERNAL,
}
}
}
#[derive(Deserialize)]
struct Envelope {
#[serde(default)]
code: Option<i64>,
#[serde(default)]
message: Option<String>,
}
pub async fn read<R: DeserializeOwned>(transport: &impl Transport, call: Call) -> Result<R, ApiError> {
let path = call.url_path();
match transport.call(call).await {
Ok(text) => decode(&path, &text),
Err(TransportError::Status { status, body }) => match refusal(&body) {
Some(refused) => Err(refused),
None => Err(ApiError::Transport(TransportError::Status { status, body })),
},
Err(error) => Err(ApiError::Transport(error)),
}
}
pub fn decode<R: DeserializeOwned>(path: &str, text: &str) -> Result<R, ApiError> {
if let Some(refused) = refusal(text) {
return Err(refused);
}
serde_json::from_str(text)
.map_err(|error| ApiError::Unreadable { path: path.to_owned(), detail: unreadable(&error, text) })
}
fn refusal(text: &str) -> Option<ApiError> {
let envelope = serde_json::from_str::<Envelope>(text).ok()?;
match envelope.code {
Some(0 | codes::OK) | None => None,
Some(code) => Some(ApiError::Refused { code, message: envelope.message.unwrap_or_default() }),
}
}
pub(crate) fn unreadable(error: &serde_json::Error, text: &str) -> String {
let shown: String = text.chars().take(300).collect();
format!("{error} in {shown}")
}
#[cfg(test)]
#[path = "tests/calls.rs"]
mod tests;