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}