zingo_price/
lib.rs

1#![warn(missing_docs)]
2
3//! Crate for fetching ZEC prices.
4//!
5//! Currently only supports USD.
6
7use std::{
8    collections::HashSet,
9    io::{Read, Write},
10    time::SystemTime,
11};
12
13use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
14
15use serde::Deserialize;
16use zcash_client_backend::tor::{self, http::cryptex::Exchanges};
17use zcash_encoding::{Optional, Vector};
18
19/// Errors with price requests and parsing.
20// TODO: remove unused when historical data is implemented
21#[derive(Debug, thiserror::Error)]
22pub enum PriceError {
23    /// Request failed.
24    #[error("request failed. {0}")]
25    RequestFailed(#[from] reqwest::Error),
26    /// Deserialization failed.
27    #[error("deserialization failed. {0}")]
28    DeserializationFailed(#[from] serde_json::Error),
29    /// Parse error.
30    #[error("parse error. {0}")]
31    ParseError(#[from] std::num::ParseFloatError),
32    /// Price list start time not set. Call `PriceList::set_start_time`.
33    #[error("price list start time has not been set.")]
34    PriceListNotInitialized,
35    /// Tor price fetch error.
36    #[error("tor price fetch error. {0}")]
37    TorError(#[from] tor::Error),
38    /// Decimal conversion error.
39    #[error("decimal conversion error. {0}")]
40    DecimalError(#[from] rust_decimal::Error),
41    /// Invalid price.
42    #[error("invalid price.")]
43    InvalidPrice,
44}
45
46#[derive(Debug, Deserialize)]
47struct CurrentPriceResponse {
48    price: String,
49    timestamp: u32,
50}
51
52/// Price of ZEC in USD at a given point in time.
53#[derive(Debug, Clone, Copy)]
54pub struct Price {
55    /// Time in seconds.
56    pub time: u32,
57    /// ZEC price in USD.
58    pub price_usd: f32,
59}
60
61/// Price list for wallets to maintain an updated list of daily ZEC prices.
62#[derive(Debug)]
63pub struct PriceList {
64    /// Current price.
65    current_price: Option<Price>,
66    /// Historical price data by day.
67    // TODO: currently unused
68    daily_prices: Vec<Price>,
69    /// Time of last historical price update in seconds.
70    // TODO: currently unused
71    time_historical_prices_last_updated: Option<u32>,
72}
73
74impl Default for PriceList {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl PriceList {
81    /// Constructs a new price list from the time of wallet creation.
82    pub fn new() -> Self {
83        PriceList {
84            current_price: None,
85            daily_prices: Vec::new(),
86            time_historical_prices_last_updated: None,
87        }
88    }
89
90    /// Returns current price.
91    pub fn current_price(&self) -> Option<Price> {
92        self.current_price
93    }
94
95    /// Returns historical price data by day.
96    pub fn daily_prices(&self) -> &[Price] {
97        &self.daily_prices
98    }
99
100    /// Returns time historical prices were last updated.
101    pub fn time_historical_prices_last_updated(&self) -> Option<u32> {
102        self.time_historical_prices_last_updated
103    }
104
105    /// Price list requires a start time before it can be updated.
106    ///
107    /// Recommended start time is the time the wallet's birthday block height was mined.
108    pub fn set_start_time(&mut self, time_of_birthday: u32) {
109        self.time_historical_prices_last_updated = Some(time_of_birthday);
110    }
111
112    /// Update and return current price of ZEC.
113    ///
114    /// Will fetch via tor if a `tor_client` is provided.
115    /// Currently only USD is supported.
116    pub async fn update_current_price(
117        &mut self,
118        tor_client: Option<&tor::Client>,
119    ) -> Result<Price, PriceError> {
120        let current_price = if let Some(client) = tor_client {
121            get_current_price_tor(client).await?
122        } else {
123            get_current_price().await?
124        };
125        self.current_price = Some(current_price);
126
127        Ok(current_price)
128    }
129
130    /// Updates historical daily price list.
131    ///
132    /// Currently only USD is supported.
133    // TODO: under development
134    pub async fn update_historical_price_list(&mut self) -> Result<(), PriceError> {
135        let current_time = SystemTime::now()
136            .duration_since(SystemTime::UNIX_EPOCH)
137            .expect("should never fail when comparing with an instant so far in the past")
138            .as_secs() as u32;
139
140        if let Some(time_last_updated) = self.time_historical_prices_last_updated {
141            self.daily_prices.append(
142                &mut get_daily_prices(
143                    time_last_updated as u128 * 1000,
144                    current_time as u128 * 1000,
145                )
146                .await?,
147            );
148        } else {
149            return Err(PriceError::PriceListNotInitialized);
150        }
151
152        self.time_historical_prices_last_updated = Some(current_time);
153
154        todo!()
155    }
156
157    /// Prunes historical price list to only retain prices for the days containing `transaction_times`.
158    ///
159    /// Will not remove prices above or equal to the `prune_below` threshold.
160    // TODO: under development
161    pub fn prune(&mut self, transaction_times: Vec<u32>, prune_below: u32) {
162        let mut relevant_days = HashSet::new();
163
164        for transaction_time in transaction_times.into_iter() {
165            for daily_price in self.daily_prices() {
166                if daily_price.time > transaction_time {
167                    assert!(daily_price.time - transaction_time < 60 * 60 * 24);
168                    relevant_days.insert(daily_price.time);
169                    break;
170                }
171            }
172        }
173
174        self.daily_prices
175            .retain(|price| relevant_days.contains(&price.time) || price.time >= prune_below)
176    }
177
178    fn serialized_version() -> u8 {
179        0
180    }
181
182    /// Deserialize into `reader`
183    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
184        let _version = reader.read_u8()?;
185
186        let time_last_updated = Optional::read(&mut reader, |r| r.read_u32::<LittleEndian>())?;
187        let current_price = Optional::read(&mut reader, |r| {
188            Ok(Price {
189                time: r.read_u32::<LittleEndian>()?,
190                price_usd: r.read_f32::<LittleEndian>()?,
191            })
192        })?;
193        let daily_prices = Vector::read(&mut reader, |r| {
194            Ok(Price {
195                time: r.read_u32::<LittleEndian>()?,
196                price_usd: r.read_f32::<LittleEndian>()?,
197            })
198        })?;
199
200        Ok(Self {
201            current_price,
202            daily_prices,
203            time_historical_prices_last_updated: time_last_updated,
204        })
205    }
206
207    /// Serialize into `writer`
208    pub fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
209        writer.write_u8(Self::serialized_version())?;
210
211        Optional::write(
212            &mut writer,
213            self.time_historical_prices_last_updated(),
214            |w, time| w.write_u32::<LittleEndian>(time),
215        )?;
216        Optional::write(&mut writer, self.current_price(), |w, price| {
217            w.write_u32::<LittleEndian>(price.time)?;
218            w.write_f32::<LittleEndian>(price.price_usd)
219        })?;
220        Vector::write(&mut writer, self.daily_prices(), |w, price| {
221            w.write_u32::<LittleEndian>(price.time)?;
222            w.write_f32::<LittleEndian>(price.price_usd)
223        })
224    }
225}
226
227/// Get current price of ZEC in USD
228async fn get_current_price() -> Result<Price, PriceError> {
229    let httpget = reqwest::get("https://api.gemini.com/v1/trades/zecusd?limit_trades=11").await?;
230    let mut trades = httpget
231        .json::<Vec<CurrentPriceResponse>>()
232        .await?
233        .iter()
234        .map(|response| {
235            let price_usd: f32 = response.price.parse()?;
236            if !price_usd.is_finite() {
237                return Err(PriceError::InvalidPrice);
238            }
239
240            Ok(Price {
241                price_usd,
242                time: response.timestamp,
243            })
244        })
245        .collect::<Result<Vec<Price>, PriceError>>()?;
246
247    trades.sort_by(|a, b| {
248        a.price_usd
249            .partial_cmp(&b.price_usd)
250            .expect("trades are checked to be finite and comparable")
251    });
252
253    Ok(trades[5])
254}
255
256/// Get current price of ZEC in USD over tor.
257async fn get_current_price_tor(tor_client: &tor::Client) -> Result<Price, PriceError> {
258    let exchanges = Exchanges::unauthenticated_known_with_gemini_trusted();
259    let current_price = tor_client.get_latest_zec_to_usd_rate(&exchanges).await?;
260    let current_time = SystemTime::now()
261        .duration_since(SystemTime::UNIX_EPOCH)
262        .expect("should never fail when comparing with an instant so far in the past")
263        .as_secs() as u32;
264
265    Ok(Price {
266        time: current_time,
267        price_usd: current_price.try_into()?,
268    })
269}
270
271/// Get daily prices in USD from `start` to `end` time in milliseconds.
272// TODO: under development
273async fn get_daily_prices(_start: u128, _end: u128) -> Result<Vec<Price>, PriceError> {
274    todo!()
275}