use anyhow::{anyhow, ensure, Result}; use lnvps_db::async_trait; use log::info; use rocket::serde::Deserialize; use schemars::JsonSchema; use serde::Serialize; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; #[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone, Copy, JsonSchema)] pub enum Currency { EUR, BTC, USD, } impl Display for Currency { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Currency::EUR => write!(f, "EUR"), Currency::BTC => write!(f, "BTC"), Currency::USD => write!(f, "USD"), } } } impl FromStr for Currency { type Err = (); fn from_str(s: &str) -> std::result::Result { match s.to_lowercase().as_str() { "eur" => Ok(Currency::EUR), "usd" => Ok(Currency::USD), "btc" => Ok(Currency::BTC), _ => Err(()), } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Ticker(pub Currency, pub Currency); impl Ticker { pub fn btc_rate(cur: &str) -> Result { let to_cur: Currency = cur.parse().map_err(|_| anyhow!("Invalid currency"))?; Ok(Ticker(Currency::BTC, to_cur)) } } impl Display for Ticker { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}/{}", self.0, self.1) } } #[derive(Debug, PartialEq)] pub struct TickerRate(pub Ticker, pub f32); #[derive(Clone, Copy, Debug, PartialEq)] pub struct CurrencyAmount(pub Currency, pub f32); impl CurrencyAmount { pub fn from_u64(currency: Currency, amount: u64) -> Self { CurrencyAmount(currency, amount as f32 / 100.0) } } impl TickerRate { pub fn can_convert(&self, currency: Currency) -> bool { currency == self.0 .0 || currency == self.0 .1 } /// Convert from the source currency into the target currency pub fn convert(&self, source: CurrencyAmount) -> Result { ensure!( self.can_convert(source.0), "Cant convert, currency doesnt match" ); if source.0 == self.0 .0 { Ok(CurrencyAmount(self.0 .1, source.1 * self.1)) } else { Ok(CurrencyAmount(self.0 .0, source.1 / self.1)) } } } #[async_trait] pub trait ExchangeRateService: Send + Sync { async fn fetch_rates(&self) -> Result>; async fn set_rate(&self, ticker: Ticker, amount: f32); async fn get_rate(&self, ticker: Ticker) -> Option; async fn list_rates(&self) -> Result>; } /// Get alternative prices based on a source price pub fn alt_prices(rates: &Vec, source: CurrencyAmount) -> Vec { let mut ret: Vec = rates .iter() .filter_map(|r| r.convert(source).ok()) .collect(); let mut ret2 = vec![]; for y in rates.iter() { for x in ret.iter() { if let Ok(r1) = y.convert(*x) { if r1.0 != source.0 { ret2.push(r1); } } } } ret.append(&mut ret2); ret } #[derive(Clone, Default)] pub struct DefaultRateCache { cache: Arc>>, } #[async_trait] impl ExchangeRateService for DefaultRateCache { async fn fetch_rates(&self) -> Result> { let rsp = reqwest::get("https://mempool.space/api/v1/prices") .await? .text() .await?; let rates: MempoolRates = serde_json::from_str(&rsp)?; let mut ret = vec![]; if let Some(usd) = rates.usd { ret.push(TickerRate(Ticker(Currency::BTC, Currency::USD), usd)); } if let Some(eur) = rates.eur { ret.push(TickerRate(Ticker(Currency::BTC, Currency::EUR), eur)); } Ok(ret) } async fn set_rate(&self, ticker: Ticker, amount: f32) { let mut cache = self.cache.write().await; info!("{}: {}", &ticker, amount); cache.insert(ticker, amount); } async fn get_rate(&self, ticker: Ticker) -> Option { let cache = self.cache.read().await; cache.get(&ticker).cloned() } async fn list_rates(&self) -> Result> { let cache = self.cache.read().await; Ok(cache.iter().map(|(k, v)| TickerRate(*k, *v)).collect()) } } #[derive(Deserialize)] struct MempoolRates { #[serde(rename = "USD")] pub usd: Option, #[serde(rename = "EUR")] pub eur: Option, } #[cfg(test)] mod tests { use super::*; const RATE: f32 = 95_000.0; #[test] fn convert() { let ticker = Ticker::btc_rate("EUR").unwrap(); let f = TickerRate(ticker, RATE); assert_eq!( f.convert(CurrencyAmount(Currency::EUR, 5.0)).unwrap(), CurrencyAmount(Currency::BTC, 5.0 / RATE) ); assert_eq!( f.convert(CurrencyAmount(Currency::BTC, 0.001)).unwrap(), CurrencyAmount(Currency::EUR, RATE * 0.001) ); assert!(!f.can_convert(Currency::USD)); assert!(f.can_convert(Currency::EUR)); assert!(f.can_convert(Currency::BTC)); } }