Skip to content

Commit

Permalink
zcash_client_backend: Add tor::Client::get_exchange_rate_usd
Browse files Browse the repository at this point in the history
This fetches the latest tickers from Binance, Coinbase, and Gemini over
Tor, and uses whichever requests are successful to determine the USD/ZEC
exchange rate.

Closes #1416.
  • Loading branch information
str4d committed Jun 19, 2024
1 parent 34ca314 commit 746e168
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ secp256k1 = "0.27"
rand = "0.8"
rand_core = "0.6"

# Currency conversions
rust_decimal = { version = "1.35", default-features = false, features = ["serde"] }

# Digests
blake2b_simd = "1"
sha2 = "0.10"
Expand Down
5 changes: 5 additions & 0 deletions zcash_client_backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ arti-client = { workspace = true, optional = true }
hyper = { workspace = true, optional = true, features = ["client", "http1"] }
serde_json = { workspace = true, optional = true }

# - Currency conversion
rust_decimal = { workspace = true, optional = true }

# Dependencies used internally:
# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.)
# - Documentation
Expand Down Expand Up @@ -176,6 +179,8 @@ tor = [
"dep:http-body-util",
"dep:hyper",
"dep:hyper-util",
"dep:rand",
"dep:rust_decimal",
"dep:serde",
"dep:serde_json",
"dep:tokio",
Expand Down
2 changes: 2 additions & 0 deletions zcash_client_backend/src/tor/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use tracing::{debug, error};

use super::{Client, Error};

mod exchange_rate;

impl Client {
async fn get<T, F: Future<Output = Result<T, Error>>>(
&self,
Expand Down
130 changes: 130 additions & 0 deletions zcash_client_backend/src/tor/http/exchange_rate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use futures_util::{join, FutureExt};
use rand::{seq::IteratorRandom, thread_rng};
use rust_decimal::Decimal;
use serde::Deserialize;
use tracing::{error, trace};

use crate::tor::{Client, Error};

impl Client {
/// Fetches the latest USD/ZEC exchange rate.
///
/// We query the ticker price of the following trading pairs:
/// - `ZECUSDT` on Binance
/// - `ZEC-USD` on Coinbase
/// - `zecusd` on Gemini
///
/// Returns an error if all three exchange queries fail.
pub async fn get_exchange_rate_usd(&self) -> Result<Decimal, Error> {
// "Never go to sea with two chronometers; take one or three."

let binance_ticker = self
.get_json::<BinanceTicker>(
"https://api.binance.com/api/v3/ticker/price?symbol=ZECUSDT"
.parse()
.unwrap(),
)
.map(|res| res.map(|resp| resp.into_body()));

let coinbase_ticker = self
.get_json::<CoinbaseTicker>(
"https://api.exchange.coinbase.com/products/ZEC-USD/ticker"
.parse()
.unwrap(),
)
.map(|res| res.map(|resp| resp.into_body()));

let gemini_ticker = self
.get_json::<GeminiTicker>("https://api.gemini.com/v2/ticker/zecusd".parse().unwrap())
.map(|res| res.map(|resp| resp.into_body()));

// Fetch the tickers in parallel.
let res = join!(binance_ticker, coinbase_ticker, gemini_ticker);
trace!(?res, "Exchange results");

match res {
// If all of the requests failed, log all errors and return one of them.
(Err(b), Err(c), Err(g)) => {
error!(binance = %b, coinbase = %c, gemini = %g, "All exchange requests failed");
Err(b)
}

// If only one request succeeded, use it.
(Ok(b), Err(_), Err(_)) => Ok(b.price),
(Err(_), Ok(c), Err(_)) => Ok(c.price),
(Err(_), Err(_), Ok(g)) => Ok(g.ask),

// If two requests succeeded, pick one at random.
(Ok(b), Ok(c), Err(_)) => Ok([b.price, c.price]
.into_iter()
.choose(&mut thread_rng())
.expect("Not empty")),
(Ok(b), Err(_), Ok(g)) => Ok([b.price, g.ask]
.into_iter()
.choose(&mut thread_rng())
.expect("Not empty")),
(Err(_), Ok(c), Ok(g)) => Ok([c.price, g.ask]
.into_iter()
.choose(&mut thread_rng())
.expect("Not empty")),

// If all three succeeded, take the median.
(Ok(b), Ok(c), Ok(g)) => {
// Hacky median calculation because f64 can be compared but not ordered.
let b_lt_c = b.price < c.ask;
let c_lt_g = c.ask < g.ask;
let b_lt_g = b.price < g.ask;
let rate = if b_lt_c {
if c_lt_g {
c.ask
} else if b_lt_g {
g.ask
} else {
b.price
}
} else if b_lt_g {
b.price
} else if c_lt_g {
g.ask
} else {
c.ask
};

Ok(rate)
}
}
}
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct BinanceTicker {
symbol: String,
price: Decimal,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CoinbaseTicker {
ask: Decimal,
bid: Decimal,
volume: Decimal,
trade_id: u32,
price: Decimal,
size: Decimal,
time: String,
rfq_volume: Decimal,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct GeminiTicker {
symbol: String,
open: Decimal,
high: Decimal,
low: Decimal,
close: Decimal,
changes: Vec<Decimal>,
bid: Decimal,
ask: Decimal,
}

0 comments on commit 746e168

Please sign in to comment.