1#![warn(missing_docs)]
2
3use 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#[derive(Debug, thiserror::Error)]
22pub enum PriceError {
23 #[error("request failed. {0}")]
25 RequestFailed(#[from] reqwest::Error),
26 #[error("deserialization failed. {0}")]
28 DeserializationFailed(#[from] serde_json::Error),
29 #[error("parse error. {0}")]
31 ParseError(#[from] std::num::ParseFloatError),
32 #[error("price list start time has not been set.")]
34 PriceListNotInitialized,
35 #[error("tor price fetch error. {0}")]
37 TorError(#[from] tor::Error),
38 #[error("decimal conversion error. {0}")]
40 DecimalError(#[from] rust_decimal::Error),
41 #[error("invalid price.")]
43 InvalidPrice,
44}
45
46#[derive(Debug, Deserialize)]
47struct CurrentPriceResponse {
48 price: String,
49 timestamp: u32,
50}
51
52#[derive(Debug, Clone, Copy)]
54pub struct Price {
55 pub time: u32,
57 pub price_usd: f32,
59}
60
61#[derive(Debug)]
63pub struct PriceList {
64 current_price: Option<Price>,
66 daily_prices: Vec<Price>,
69 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 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 pub fn current_price(&self) -> Option<Price> {
92 self.current_price
93 }
94
95 pub fn daily_prices(&self) -> &[Price] {
97 &self.daily_prices
98 }
99
100 pub fn time_historical_prices_last_updated(&self) -> Option<u32> {
102 self.time_historical_prices_last_updated
103 }
104
105 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 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 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 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 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 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
227async 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
256async 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
271async fn get_daily_prices(_start: u128, _end: u128) -> Result<Vec<Price>, PriceError> {
274 todo!()
275}