#![allow(clippy::unwrap_used, clippy::expect_used)]
use chrono::{DateTime, Utc};
use hmac::{Hmac, KeyInit, Mac};
use reqwest::{RequestBuilder, StatusCode};
use rustrade_instrument::asset::name::AssetNameInternal;
use rustrade_integration::{
error::SocketError,
protocol::http::{
HttpParser,
private::{RequestSigner, Signer, encoder::HexEncoder},
rest::{RestRequest, client::RestClient},
},
};
use serde::Deserialize;
use std::borrow::Cow;
use thiserror::Error;
struct FtxSigner {
api_key: String,
}
struct FtxSignConfig<'a> {
api_key: &'a str,
time: DateTime<Utc>,
method: reqwest::Method,
path: Cow<'static, str>,
}
impl Signer for FtxSigner {
type Config<'a>
= FtxSignConfig<'a>
where
Self: 'a;
fn config<'a, Request>(
&'a self,
request: Request,
_: &RequestBuilder,
) -> Result<Self::Config<'a>, SocketError>
where
Request: RestRequest,
{
Ok(FtxSignConfig {
api_key: self.api_key.as_str(),
time: Utc::now(),
method: Request::method(),
path: request.path(),
})
}
fn add_bytes_to_sign<M>(mac: &mut M, config: &Self::Config<'_>)
where
M: Mac,
{
mac.update(config.time.to_string().as_bytes());
mac.update(config.method.as_str().as_bytes());
mac.update(config.path.as_bytes());
}
fn build_signed_request(
config: Self::Config<'_>,
builder: RequestBuilder,
signature: String,
) -> Result<reqwest::Request, SocketError> {
builder
.header("FTX-KEY", config.api_key)
.header("FTX-TS", &config.time.timestamp_millis().to_string())
.header("FTX-SIGN", &signature)
.build()
.map_err(SocketError::from)
}
}
struct FtxParser;
impl HttpParser for FtxParser {
type ApiError = serde_json::Value;
type OutputError = ExecutionError;
fn parse_api_error(&self, status: StatusCode, api_error: Self::ApiError) -> Self::OutputError {
let error = api_error.to_string();
match error.as_str() {
message if message.contains("Invalid login credentials") => {
ExecutionError::Unauthorised(error)
}
_ => ExecutionError::Socket(SocketError::HttpResponse(status, error)),
}
}
}
#[derive(Debug, Error)]
enum ExecutionError {
#[error("request authorisation invalid: {0}")]
Unauthorised(String),
#[error("SocketError: {0}")]
Socket(#[from] SocketError),
}
struct FetchBalancesRequest;
impl RestRequest for FetchBalancesRequest {
type Response = FetchBalancesResponse; type QueryParams = (); type Body = ();
fn path(&self) -> Cow<'static, str> {
Cow::Borrowed("/api/wallet/balances")
}
fn method() -> reqwest::Method {
reqwest::Method::GET
}
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct FetchBalancesResponse {
success: bool,
result: Vec<FtxBalance>,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct FtxBalance {
#[serde(rename = "coin")]
symbol: AssetNameInternal,
total: f64,
}
#[tokio::main]
async fn main() {
let mac: Hmac<sha2::Sha256> = Hmac::new_from_slice("api_secret".as_bytes()).unwrap();
let request_signer = RequestSigner::new(
FtxSigner {
api_key: "api_key".to_string(),
},
mac,
HexEncoder,
);
let rest_client = RestClient::new("https://ftx.com", request_signer, FtxParser);
let _response = rest_client.execute(FetchBalancesRequest).await;
}