#![forbid(unsafe_code)]
extern crate base64;
extern crate chrono;
extern crate hmac;
extern crate hyper;
extern crate hyper_tls;
extern crate serde;
extern crate sha2;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
type DateTime = chrono::DateTime<chrono::Utc>;
#[derive(Serialize, Deserialize, Debug)]
pub struct Decimal(String);
macro_rules! impl_num_from {
($t:ty) => {
impl From<$t> for Decimal {
fn from(x : $t) -> Self { Decimal(x.to_string()) }
}
};
}
impl_num_from!(f32);
impl_num_from!(f64);
impl_num_from!(i8);
impl_num_from!(i16);
impl_num_from!(i32);
impl_num_from!(i64);
impl_num_from!(isize);
impl_num_from!(u8);
impl_num_from!(u16);
impl_num_from!(u32);
impl_num_from!(u64);
impl_num_from!(usize);
use std::str::FromStr;
impl Decimal {
pub fn to_f32(&self) -> Option<f32> {
match self {
Decimal(s) => match f32::from_str(s) {
Ok(f) => Some(f),
Err(_) => None,
},
}
}
pub fn to_f64(&self) -> Option<f64> {
match self {
Decimal(s) => match f64::from_str(s) {
Ok(f) => Some(f),
Err(_) => None,
},
}
}
}
#[derive(Deserialize, Debug)]
pub struct Product {
pub id : String,
pub base_currency : String,
pub quote_currency : String,
pub base_min_size : Decimal,
pub base_max_size : Decimal,
pub quote_increment : Decimal,
}
#[derive(Deserialize, Debug)]
pub struct AggregatedBook {
pub sequence : u64,
pub bids : Vec<(Decimal, Decimal, u64)>,
pub asks : Vec<(Decimal, Decimal, u64)>,
}
#[derive(Deserialize, Debug)]
pub struct FullBook {
pub sequence : u64,
pub bids : Vec<(Decimal, Decimal, String)>,
pub asks : Vec<(Decimal, Decimal, String)>,
}
pub mod book_level {
use AggregatedBook;
use FullBook;
pub struct Best();
pub struct Top50();
pub struct Full();
pub trait BookLevel<T> {
fn to_str(&self) -> &str;
}
impl BookLevel<AggregatedBook> for Best {
fn to_str(&self) -> &str { "level=1" }
}
impl BookLevel<AggregatedBook> for Top50 {
fn to_str(&self) -> &str { "level=2" }
}
impl BookLevel<FullBook> for Full {
fn to_str(&self) -> &str { "level=3" }
}
}
#[derive(Deserialize, Debug)]
pub struct Ticker {
pub trade_id : u64,
pub price : Decimal,
pub size : Decimal,
pub bid : Decimal,
pub ask : Decimal,
pub volume : Decimal,
pub time : DateTime,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Side {
Buy,
Sell,
}
#[derive(Deserialize, Debug)]
pub struct Trade {
pub time : DateTime,
pub trade_id : u64,
pub price : Decimal,
pub size : Decimal,
pub side : Side,
}
#[derive(Deserialize, Debug)]
pub struct Candle(u64, f64, f64, f64, f64, f64);
#[derive(Deserialize, Debug)]
pub struct Stats {
pub open : Decimal,
pub high : Decimal,
pub low : Decimal,
pub volume : Decimal,
}
#[derive(Deserialize, Debug)]
pub struct Currency {
pub id : String,
pub name : String,
pub min_size : Decimal,
}
#[derive(Deserialize, Debug)]
pub struct ServerTime {
pub iso : DateTime,
pub epoch : f64,
}
pub const SANDBOX : &str = "https://api-public.sandbox.pro.coinbase.com";
pub const LIVE : &str = "https://api.pro.coinbase.com";
use hyper::{
rt::{Future, Stream},
Client, Request,
};
use hyper_tls::HttpsConnector;
#[derive(Debug)]
pub enum Error {
HttpError(hyper::error::Error),
JsonError(
serde_json::Error,
Result<String, std::string::FromUtf8Error>,
),
}
pub struct MarketDataClient {
client : Client<HttpsConnector<hyper::client::HttpConnector>, hyper::Body>,
base : &'static str,
}
impl MarketDataClient {
pub fn new(base : &'static str) -> Result<Self, hyper_tls::Error> {
let mut https = HttpsConnector::new(4)?;
https.https_only(true);
Ok(MarketDataClient {
client : Client::builder().build::<_, hyper::Body>(https),
base,
})
}
fn get<T>(&self, uri : &str) -> impl Future<Item = T, Error = Error>
where
T : serde::de::DeserializeOwned,
{
let req = Request::builder()
.uri(uri)
.header(hyper::header::USER_AGENT, "coinbase-api-rust")
.body(hyper::Body::empty())
.unwrap();
self
.client
.request(req)
.and_then(|res| res.into_body().concat2())
.map_err(Error::HttpError)
.and_then(|body| {
serde_json::from_slice(body.as_ref())
.map_err(|err| Error::JsonError(err, String::from_utf8(body.as_ref().to_vec())))
})
}
pub fn products(&self) -> impl Future<Item = Vec<Product>, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/products");
self.get(&uri)
}
pub fn book<T>(
&self,
product_id : &str,
level : &book_level::BookLevel<T>,
) -> impl Future<Item = T, Error = Error>
where
T : serde::de::DeserializeOwned,
{
let mut uri = self.base.to_string();
uri.push_str("/products/");
uri.push_str(product_id);
uri.push_str("/book?");
uri.push_str(level.to_str());
self.get(&uri)
}
pub fn ticker(&self, product_id : &str) -> impl Future<Item = Ticker, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/products/");
uri.push_str(product_id);
uri.push_str("/ticker");
self.get(&uri)
}
pub fn trades(&self, product_id : &str) -> impl Future<Item = Vec<Trade>, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/products/");
uri.push_str(product_id);
uri.push_str("/trades");
self.get(&uri)
}
pub fn candles(
&self,
product_id : &str,
start : &DateTime,
end : &DateTime,
granularity : &chrono::Duration,
) -> impl Future<Item = Vec<Candle>, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/products/");
uri.push_str(product_id);
uri.push_str("/candles?start=");
uri.push_str(&start.to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
uri.push_str("&end=");
uri.push_str(&end.to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
uri.push_str("&granularity=");
uri.push_str(&granularity.num_seconds().to_string());
self.get(&uri)
}
pub fn latest_candles(
&self,
product_id : &str,
granularity : &chrono::Duration,
) -> impl Future<Item = Vec<Candle>, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/products/");
uri.push_str(product_id);
uri.push_str("/candles?granularity=");
uri.push_str(&granularity.num_seconds().to_string());
self.get(&uri)
}
pub fn stats(&self, product_id : &str) -> impl Future<Item = Stats, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/products/");
uri.push_str(product_id);
uri.push_str("/stats");
self.get(&uri)
}
pub fn currencies(&self) -> impl Future<Item = Vec<Currency>, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/currencies");
self.get(&uri)
}
pub fn time(&self) -> impl Future<Item = ServerTime, Error = Error> {
let mut uri = self.base.to_string();
uri.push_str("/time");
self.get(&uri)
}
}
#[derive(Deserialize, Debug)]
pub struct Account {
pub id : String,
pub currency : String,
pub balance : Decimal,
pub available : Decimal,
pub hold : Decimal,
pub profile_id : String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum ActivityType {
Transfer,
Match,
Fee,
Rebate,
}
#[derive(Deserialize, Debug)]
pub struct ActivityDetails {
pub order_id : Option<String>,
pub trade_id : Option<String>,
pub product_id : Option<String>,
pub transfer_id : Option<String>,
pub transfer_type : Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct Activity {
pub id : u64,
pub created_at : DateTime,
pub amount : Decimal,
pub balance : Decimal,
#[serde(rename = "type")]
pub activity_type : ActivityType,
pub details : ActivityDetails,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum HoldType {
Order,
Transfer,
}
#[derive(Deserialize, Debug)]
pub struct Hold {
pub id : String,
pub created_at : DateTime,
pub updated_at : Option<DateTime>,
pub amount : Decimal,
#[serde(rename = "type")]
pub hold_type : HoldType,
#[serde(rename = "ref")]
pub hold_ref : String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum OrderType {
Limit,
Market,
}
#[derive(Deserialize, Debug)]
pub struct Order {
pub id : String,
pub price : Option<Decimal>,
pub size : Option<Decimal>,
pub product_id : String,
pub side : Side,
pub stp : Option<String>,
pub funds : Option<Decimal>,
pub specified_funds : Option<Decimal>,
#[serde(rename = "type")]
pub order_type : OrderType,
pub time_in_force : Option<String>,
pub post_only : bool,
pub created_at : DateTime,
pub done_at : Option<DateTime>,
pub done_reason : Option<String>,
pub fill_fees : Decimal,
pub filled_size : Decimal,
pub executed_value : Decimal,
pub status : String,
pub settled : bool,
}
#[derive(Deserialize, Debug)]
pub struct Fill {
pub trade_id : u64,
pub product_id : String,
pub price : Decimal,
pub size : Decimal,
pub order_id : String,
pub created_at : DateTime,
pub liquidity : String,
pub fee : Decimal,
pub settled : bool,
pub side : Side,
}
#[derive(Deserialize, Debug)]
pub struct TrailingVolume {
pub product_id : String,
pub exchange_volume : Decimal,
pub volume : Decimal,
pub recorded_at : DateTime,
}
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
pub struct PrivateClient {
client : Client<HttpsConnector<hyper::client::HttpConnector>, hyper::Body>,
base : &'static str,
key : String,
secret : String,
passphrase : String,
}
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
impl PrivateClient {
pub fn new(
base : &'static str,
key : String,
secret : String,
passphrase : String,
) -> Result<Self, hyper_tls::Error> {
let mut https = HttpsConnector::new(4)?;
https.https_only(true);
Ok(PrivateClient {
client : Client::builder().build::<_, hyper::Body>(https),
base,
key,
secret,
passphrase,
})
}
fn sign(&self, timestamp : u64, method : &hyper::Method, path : &str, body : &str) -> String {
let mut message = timestamp.to_string();
message.push_str(method.as_str());
message.push_str(path);
message.push_str(body);
let mut mac = HmacSha256::new_varkey(&base64::decode(&self.secret).unwrap()).unwrap();
mac.input(message.as_bytes());
base64::encode(&mac.result().code())
}
fn get<T>(&self, query : &str) -> impl Future<Item = T, Error = Error>
where
T : serde::de::DeserializeOwned,
{
let mut uri = self.base.to_string();
uri.push_str(query);
let timestamp = now();
let req = Request::builder()
.uri(uri)
.header(hyper::header::USER_AGENT, "coinbase-api-rust")
.header("cb-access-key", self.key.as_str())
.header("cb-access-passphrase", self.passphrase.as_str())
.header("cb-access-timestamp", timestamp)
.header(
"cb-access-sign",
self
.sign(timestamp, &hyper::Method::GET, query, "")
.as_str(),
).body(hyper::Body::empty())
.unwrap();
self
.client
.request(req)
.and_then(|res| res.into_body().concat2())
.map_err(Error::HttpError)
.and_then(|body| {
serde_json::from_slice(body.as_ref())
.map_err(|err| Error::JsonError(err, String::from_utf8(body.as_ref().to_vec())))
})
}
pub fn accounts(&self) -> impl Future<Item = Vec<Account>, Error = Error> {
self.get("/accounts")
}
pub fn account(&self, id : &str) -> impl Future<Item = Account, Error = Error> {
let mut query = "/accounts/".to_string();
query.push_str(id);
self.get(&query)
}
pub fn ledger(&self, id : &str) -> impl Future<Item = Vec<Activity>, Error = Error> {
let mut query = "/accounts/".to_string();
query.push_str(id);
query.push_str("/ledger");
self.get(&query)
}
pub fn holds(&self, id : &str) -> impl Future<Item = Vec<Hold>, Error = Error> {
let mut query = "/accounts/".to_string();
query.push_str(id);
query.push_str("/holds");
self.get(&query)
}
pub fn orders(&self) -> impl Future<Item = Vec<Order>, Error = Error> {
self.get("/orders?status=all")
}
pub fn orders_for_product(&self, id : &str) -> impl Future<Item = Vec<Order>, Error = Error> {
let mut query = "/orders?status=all&product_id=".to_string();
query.push_str(id);
self.get(&query)
}
pub fn order(&self, id : &str) -> impl Future<Item = Order, Error = Error> {
let mut query = "/orders/".to_string();
query.push_str(id);
self.get(&query)
}
pub fn fills(&self) -> impl Future<Item = Vec<Fill>, Error = Error> { self.get("/fills") }
pub fn fills_for_product(&self, id : &str) -> impl Future<Item = Vec<Fill>, Error = Error> {
let mut query = "/fills?product_id=".to_string();
query.push_str(id);
self.get(&query)
}
pub fn fills_for_order(&self, id : &str) -> impl Future<Item = Vec<Fill>, Error = Error> {
let mut query = "/fills?order_id=".to_string();
query.push_str(id);
self.get(&query)
}
pub fn trailing_volume(&self) -> impl Future<Item = Vec<TrailingVolume>, Error = Error> {
self.get("/users/self/trailing-volume")
}
}