zcash_client_backend: Add exchanges we know how to query for USD/ZEC

Currently only unauthenticated connections are supported (i.e. no API
keys can be configured). However, AFAICT none of these exchanges provide
non-IP-based rate limits for authenticated connections to public APIs,
so authentication wouldn't help to make connections over Tor more
reliable.
This commit is contained in:
Jack Grigg 2024-07-18 00:38:39 +00:00
parent 739e878c8b
commit ac5bac9969
7 changed files with 379 additions and 0 deletions

View File

@ -8,6 +8,23 @@ use tracing::{error, trace};
use crate::tor::{Client, Error};
mod binance;
mod coinbase;
mod gate_io;
mod gemini;
mod ku_coin;
mod mexc;
/// Exchanges for which we know how to query data over Tor.
pub mod exchanges {
pub use super::binance::Binance;
pub use super::coinbase::Coinbase;
pub use super::gate_io::GateIo;
pub use super::gemini::Gemini;
pub use super::ku_coin::KuCoin;
pub use super::mexc::Mexc;
}
/// An exchange that can be queried for ZEC data.
#[async_trait]
pub trait Exchange: 'static {
@ -43,6 +60,20 @@ pub struct Exchanges {
}
impl Exchanges {
/// Unauthenticated connections to all known exchanges with USD/ZEC pairs.
///
/// Gemini is treated as a "trusted" data source due to being a NYDFS-regulated
/// exchange.
pub fn unauthenticated_known_with_gemini_trusted() -> Self {
Self::builder(exchanges::Gemini::unauthenticated())
.with(exchanges::Binance::unauthenticated())
.with(exchanges::Coinbase::unauthenticated())
.with(exchanges::GateIo::unauthenticated())
.with(exchanges::KuCoin::unauthenticated())
.with(exchanges::Mexc::unauthenticated())
.build()
}
/// Returns an `Exchanges` builder.
///
/// The `trusted` exchange will always have its data used, _if_ data is successfully

View File

@ -0,0 +1,65 @@
use async_trait::async_trait;
use rust_decimal::Decimal;
use serde::Deserialize;
use super::{Exchange, ExchangeData};
use crate::tor::{Client, Error};
/// Querier for the Binance exchange.
pub struct Binance {
_private: (),
}
impl Binance {
/// Prepares for unauthenticated connections to Binance.
pub fn unauthenticated() -> Self {
Self { _private: () }
}
}
#[derive(Clone, Debug, Deserialize)]
#[allow(dead_code)]
#[allow(non_snake_case)]
struct BinanceData {
symbol: String,
priceChange: Decimal,
priceChangePercent: Decimal,
weightedAvgPrice: Decimal,
prevClosePrice: Decimal,
lastPrice: Decimal,
lastQty: Decimal,
bidPrice: Decimal,
bidQty: Decimal,
askPrice: Decimal,
askQty: Decimal,
openPrice: Decimal,
highPrice: Decimal,
lowPrice: Decimal,
volume: Decimal,
quoteVolume: Decimal,
openTime: u64,
closeTime: u64,
firstId: u32,
lastId: u32,
count: u32,
}
#[async_trait]
impl Exchange for Binance {
async fn query_zec_to_usd(&self, client: &Client) -> Result<ExchangeData, Error> {
// API documentation:
// https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics
let res = client
.get_json::<BinanceData>(
"https://api.binance.com/api/v3/ticker/24hr?symbol=ZECUSDT"
.parse()
.unwrap(),
)
.await?;
let data = res.into_body();
Ok(ExchangeData {
bid: data.bidPrice,
ask: data.askPrice,
})
}
}

View File

@ -0,0 +1,53 @@
use async_trait::async_trait;
use rust_decimal::Decimal;
use serde::Deserialize;
use super::{Exchange, ExchangeData};
use crate::tor::{Client, Error};
/// Querier for the Coinbase exchange.
pub struct Coinbase {
_private: (),
}
impl Coinbase {
/// Prepares for unauthenticated connections to Coinbase.
pub fn unauthenticated() -> Self {
Self { _private: () }
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CoinbaseData {
ask: Decimal,
bid: Decimal,
volume: Decimal,
trade_id: u32,
price: Decimal,
size: Decimal,
time: String,
rfq_volume: Option<Decimal>,
conversions_volume: Option<Decimal>,
}
#[async_trait]
impl Exchange for Coinbase {
#[allow(dead_code)]
async fn query_zec_to_usd(&self, client: &Client) -> Result<ExchangeData, Error> {
// API documentation:
// https://docs.cdp.coinbase.com/exchange/reference/exchangerestapi_getproductticker
let res = client
.get_json::<CoinbaseData>(
"https://api.exchange.coinbase.com/products/ZEC-USD/ticker"
.parse()
.unwrap(),
)
.await?;
let data = res.into_body();
Ok(ExchangeData {
bid: data.bid,
ask: data.ask,
})
}
}

View File

@ -0,0 +1,56 @@
use async_trait::async_trait;
use hyper::StatusCode;
use rust_decimal::Decimal;
use serde::Deserialize;
use super::{Exchange, ExchangeData};
use crate::tor::{Client, Error};
/// Querier for the Gate.io exchange.
pub struct GateIo {
_private: (),
}
impl GateIo {
/// Prepares for unauthenticated connections to Gate.io.
pub fn unauthenticated() -> Self {
Self { _private: () }
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct GateIoData {
currency_pair: String,
last: Decimal,
lowest_ask: Decimal,
highest_bid: Decimal,
change_percentage: Decimal,
base_volume: Decimal,
quote_volume: Decimal,
high_24h: Decimal,
low_24h: Decimal,
}
#[async_trait]
impl Exchange for GateIo {
async fn query_zec_to_usd(&self, client: &Client) -> Result<ExchangeData, Error> {
// API documentation:
// https://www.gate.io/docs/developers/apiv4/#retrieve-ticker-information
let res = client
.get_json::<Vec<GateIoData>>(
"https://api.gateio.ws/api/v4/spot/tickers?currency_pair=ZEC_USDT"
.parse()
.unwrap(),
)
.await?;
let data = res.into_body().into_iter().next().ok_or(Error::Http(
super::super::HttpError::Unsuccessful(StatusCode::GONE),
))?;
Ok(ExchangeData {
bid: data.highest_bid,
ask: data.lowest_ask,
})
}
}

View File

@ -0,0 +1,47 @@
use async_trait::async_trait;
use rust_decimal::Decimal;
use serde::Deserialize;
use super::{Exchange, ExchangeData};
use crate::tor::{Client, Error};
/// Querier for the Gemini exchange.
pub struct Gemini {
_private: (),
}
impl Gemini {
/// Prepares for unauthenticated connections to Gemini.
pub fn unauthenticated() -> Self {
Self { _private: () }
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct GeminiData {
symbol: String,
open: Decimal,
high: Decimal,
low: Decimal,
close: Decimal,
changes: Vec<Decimal>,
bid: Decimal,
ask: Decimal,
}
#[async_trait]
impl Exchange for Gemini {
async fn query_zec_to_usd(&self, client: &Client) -> Result<ExchangeData, Error> {
// API documentation:
// https://docs.gemini.com/rest-api/#ticker-v2
let res = client
.get_json::<GeminiData>("https://api.gemini.com/v2/ticker/zecusd".parse().unwrap())
.await?;
let data = res.into_body();
Ok(ExchangeData {
bid: data.bid,
ask: data.ask,
})
}
}

View File

@ -0,0 +1,67 @@
use async_trait::async_trait;
use rust_decimal::Decimal;
use serde::Deserialize;
use super::{Exchange, ExchangeData};
use crate::tor::{Client, Error};
/// Querier for the KuCoin exchange.
pub struct KuCoin {
_private: (),
}
impl KuCoin {
/// Prepares for unauthenticated connections to KuCoin.
pub fn unauthenticated() -> Self {
Self { _private: () }
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
#[allow(non_snake_case)]
struct KuCoinData {
time: u64,
symbol: String,
buy: Decimal,
sell: Decimal,
changeRate: Decimal,
changePrice: Decimal,
high: Decimal,
low: Decimal,
vol: Decimal,
volValue: Decimal,
last: Decimal,
averagePrice: Decimal,
takerFeeRate: Decimal,
makerFeeRate: Decimal,
takerCoefficient: Decimal,
makerCoefficient: Decimal,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct KuCoinResponse {
code: String,
data: KuCoinData,
}
#[async_trait]
impl Exchange for KuCoin {
async fn query_zec_to_usd(&self, client: &Client) -> Result<ExchangeData, Error> {
// API documentation:
// https://www.kucoin.com/docs/rest/spot-trading/market-data/get-24hr-stats
let res = client
.get_json::<KuCoinResponse>(
"https://api.kucoin.com/api/v1/market/stats?symbol=ZEC-USDT"
.parse()
.unwrap(),
)
.await?;
let data = res.into_body().data;
Ok(ExchangeData {
bid: data.buy,
ask: data.sell,
})
}
}

View File

@ -0,0 +1,60 @@
use async_trait::async_trait;
use rust_decimal::Decimal;
use serde::Deserialize;
use super::{Exchange, ExchangeData};
use crate::tor::{Client, Error};
/// Querier for the MEXC exchange.
pub struct Mexc {
_private: (),
}
impl Mexc {
/// Prepares for unauthenticated connections to MEXC.
pub fn unauthenticated() -> Self {
Self { _private: () }
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
#[allow(non_snake_case)]
struct MexcData {
symbol: String,
priceChange: Decimal,
priceChangePercent: Decimal,
prevClosePrice: Decimal,
lastPrice: Decimal,
bidPrice: Decimal,
bidQty: Decimal,
askPrice: Decimal,
askQty: Decimal,
openPrice: Decimal,
highPrice: Decimal,
lowPrice: Decimal,
volume: Decimal,
quoteVolume: Decimal,
openTime: u64,
closeTime: u64,
}
#[async_trait]
impl Exchange for Mexc {
async fn query_zec_to_usd(&self, client: &Client) -> Result<ExchangeData, Error> {
// API documentation:
// https://mexcdevelop.github.io/apidocs/spot_v3_en/#24hr-ticker-price-change-statistics
let res = client
.get_json::<MexcData>(
"https://api.mexc.com/api/v3/ticker/24hr?symbol=ZECUSDT"
.parse()
.unwrap(),
)
.await?;
let data = res.into_body();
Ok(ExchangeData {
bid: data.bidPrice,
ask: data.askPrice,
})
}
}