Skip to main content

zcash_client_backend/tor/http/
cryptex.rs

1//! Cryptocurrency exchange rate APIs.
2
3use futures_util::{future::join_all, join};
4use hyper::StatusCode;
5use rand::{Rng, seq::IteratorRandom};
6use rust_decimal::Decimal;
7use tracing::{error, trace};
8
9use crate::tor::{Client, Error};
10
11use super::Retry;
12
13mod binance;
14mod coin_ex;
15mod coinbase;
16mod digi_finex;
17mod gemini;
18mod kraken;
19mod ku_coin;
20mod mexc;
21mod xt;
22
23/// Maximum number of retries for exchange queries implemented in this crate.
24const RETRY_LIMIT: u8 = 1;
25
26/// A retry filter that attempts to avoid Tor exit node connection failures.
27///
28/// A common failure with HTTP requests over Tor is a particular exit node being blocked
29/// by the server. This may be due to other exit node activity causing some IP rate limit
30/// to be exceeded, for example.
31///
32/// If the HTTP request doesn't require a persistent Tor client identity across queries,
33/// we can retry with an isolated client in order to use new circuits that have a decent
34/// chance of using a different exit node. The isolation is not for privacy; the server
35/// can trivially link the two requests together via timing.
36///
37/// This filter attempts retries as follows:
38/// - A successful request that resulted in a client error (HTTP 400-499) will cause a
39///   retry with an isolated client.
40/// - All other errors will cause a retry with the same client.
41fn retry_filter(res: Result<StatusCode, &Error>) -> Option<Retry> {
42    match res {
43        Ok(status) => {
44            if status.is_client_error() {
45                Some(Retry::Isolated)
46            } else {
47                (!status.is_success()).then_some(Retry::Same)
48            }
49        }
50        Err(_) => Some(Retry::Same),
51    }
52}
53
54/// Exchanges for which we know how to query data over Tor.
55///
56/// Queries to these exchanges will be retried a single time on error.
57pub mod exchanges {
58    pub use super::binance::Binance;
59    pub use super::coin_ex::CoinEx;
60    pub use super::coinbase::Coinbase;
61    pub use super::digi_finex::DigiFinex;
62    pub use super::gemini::Gemini;
63    pub use super::kraken::Kraken;
64    pub use super::ku_coin::KuCoin;
65    pub use super::mexc::Mexc;
66    pub use super::xt::Xt;
67}
68
69/// An exchange that can be queried for ZEC data.
70#[trait_variant::make(Exchange: Send)]
71#[dynosaur::dynosaur(DynExchange = dyn(box) Exchange)]
72pub trait LocalExchange {
73    /// Queries data about the USD/ZEC pair.
74    ///
75    /// The returned bid and ask data must be denominated in USD, i.e. the latest bid and
76    /// ask for 1 ZEC.
77    async fn query_zec_to_usd(&self, client: &Client) -> Result<ExchangeData, Error>;
78}
79
80/// Data queried from an [`Exchange`].
81#[derive(Debug)]
82pub struct ExchangeData {
83    /// The highest current bid.
84    pub bid: Decimal,
85
86    /// The lowest current ask.
87    pub ask: Decimal,
88}
89
90impl ExchangeData {
91    /// Returns the mid-point between current best bid and current best ask, to avoid
92    /// manipulation by targeted trade fulfilment.
93    fn exchange_rate(&self) -> Decimal {
94        (self.bid + self.ask) / Decimal::TWO
95    }
96}
97
98/// A set of [`Exchange`]s that can be queried for ZEC data.
99pub struct Exchanges {
100    trusted: Box<DynExchange<'static>>,
101    others: Vec<Box<DynExchange<'static>>>,
102}
103
104impl Exchanges {
105    /// Unauthenticated connections to all known exchanges with USD/ZEC pairs.
106    ///
107    /// Gemini is treated as a "trusted" data source due to being a NYDFS-regulated
108    /// exchange.
109    pub fn unauthenticated_known_with_gemini_trusted() -> Self {
110        Self::builder(exchanges::Gemini::unauthenticated())
111            .with(exchanges::Binance::unauthenticated())
112            .with(exchanges::CoinEx::unauthenticated())
113            .with(exchanges::Coinbase::unauthenticated())
114            .with(exchanges::DigiFinex::unauthenticated())
115            .with(exchanges::Kraken::unauthenticated())
116            .with(exchanges::KuCoin::unauthenticated())
117            .with(exchanges::Mexc::unauthenticated())
118            .with(exchanges::Xt::unauthenticated())
119            .build()
120    }
121
122    /// Returns an `Exchanges` builder.
123    ///
124    /// The `trusted` exchange will always have its data used, _if_ data is successfully
125    /// obtained via Tor (i.e. no transient failures).
126    pub fn builder(trusted: impl Exchange + 'static) -> ExchangesBuilder {
127        ExchangesBuilder::new(trusted)
128    }
129}
130
131/// Builder type for [`Exchanges`].
132///
133/// Every [`Exchanges`] is configured with a "trusted" [`Exchange`] that will always have
134/// its data used, if data is successfully obtained via Tor (i.e. no transient failures).
135/// Additional data sources can be provided to [`ExchangesBuilder::with`] for resiliency
136/// against transient network failures or adversarial market manipulation on individual
137/// sources.
138///
139/// The number of times [`ExchangesBuilder::with`] is called will affect the behaviour of
140/// the final [`Exchanges`]:
141/// - With no additional sources, the trusted [`Exchange`] is used on its own.
142/// - With one additional source, the trusted [`Exchange`] is used preferentially,
143///   with the additional source as a backup if the trusted source cannot be queried.
144/// - With two or more additional sources, a minimum of three successful responses are
145///   required from any of the sources.
146pub struct ExchangesBuilder(Exchanges);
147
148impl ExchangesBuilder {
149    /// Constructs a new [`Exchanges`] builder.
150    ///
151    /// The `trusted` exchange will always have its data used, _if_ data is successfully
152    /// obtained via Tor (i.e. no transient failures).
153    pub fn new(trusted: impl Exchange + 'static) -> Self {
154        Self(Exchanges {
155            trusted: DynExchange::new_box(trusted),
156            others: vec![],
157        })
158    }
159
160    /// Adds another [`Exchange`] as a data source.
161    pub fn with(mut self, other: impl Exchange + 'static) -> Self {
162        self.0.others.push(DynExchange::new_box(other));
163        self
164    }
165
166    /// Builds the [`Exchanges`].
167    pub fn build(self) -> Exchanges {
168        self.0
169    }
170}
171
172impl Client {
173    /// Fetches the latest USD/ZEC exchange rate, derived from the given exchanges.
174    ///
175    /// `rng` chooses which rate to drop when an odd number of rates is needed.
176    ///
177    /// Returns:
178    /// - `Ok(rate)` if at least one exchange request succeeds.
179    /// - `Err(_)` if none of the exchange queries succeed.
180    pub async fn get_latest_zec_to_usd_rate(
181        &self,
182        rng: &mut impl Rng,
183        exchanges: &Exchanges,
184    ) -> Result<Decimal, Error> {
185        self.ensure_bootstrapped().await?;
186
187        // Fetch the data in parallel.
188        let res = join!(
189            DynExchange::query_zec_to_usd(&exchanges.trusted, self),
190            join_all(
191                exchanges
192                    .others
193                    .iter()
194                    .map(|e| DynExchange::query_zec_to_usd(e, self))
195            )
196        );
197        trace!(?res, "Data results");
198        let (trusted_res, other_res) = res;
199
200        // Split into successful queries and errors.
201        let mut rates: Vec<Decimal> = vec![];
202        let mut errors = vec![];
203        for res in other_res {
204            match res {
205                Ok(d) => rates.push(d.exchange_rate()),
206                Err(e) => errors.push(e),
207            }
208        }
209
210        // "Never go to sea with two chronometers; take one or three."
211        // Randomly drop one rate if necessary to have an odd number of rates, as long as
212        // we have either at least three rates, or fewer than three sources.
213        if exchanges.others.len() >= 2 && rates.len() + usize::from(trusted_res.is_ok()) < 3 {
214            error!("Too many exchange requests failed");
215            return Err(errors
216                .into_iter()
217                .next()
218                .expect("At least one request failed"));
219        }
220        let mut evict_random = |s: &mut Vec<Decimal>| {
221            if let Some(index) = (0..s.len()).choose(&mut *rng) {
222                s.remove(index);
223            }
224        };
225        match trusted_res {
226            Ok(trusted) => {
227                if !rates.len().is_multiple_of(2) {
228                    evict_random(&mut rates);
229                }
230                rates.push(trusted.exchange_rate());
231            }
232            Err(e) => {
233                if rates.len().is_multiple_of(2) {
234                    evict_random(&mut rates);
235                }
236                errors.push(e);
237            }
238        }
239
240        // If all of the requests failed, log all errors and return one of them.
241        if rates.is_empty() {
242            error!("All exchange requests failed");
243            Err(errors.into_iter().next().expect("All requests failed"))
244        } else {
245            // We have an odd number of rates; take the median.
246            assert!(!rates.len().is_multiple_of(2));
247            rates.sort();
248            let median = rates.len() / 2;
249            Ok(rates[median])
250        }
251    }
252}