diff --git a/core/src/datapoint_source.rs b/core/src/datapoint_source.rs index b3fdc089..9802a7ca 100644 --- a/core/src/datapoint_source.rs +++ b/core/src/datapoint_source.rs @@ -6,6 +6,7 @@ mod bitpanda; mod coincap; mod coingecko; mod custom_ext_script; +mod erg_btc; mod erg_usd; mod erg_xau; mod predef; diff --git a/core/src/datapoint_source/assets_exchange_rate.rs b/core/src/datapoint_source/assets_exchange_rate.rs index 1f90aee1..9f412220 100644 --- a/core/src/datapoint_source/assets_exchange_rate.rs +++ b/core/src/datapoint_source/assets_exchange_rate.rs @@ -9,9 +9,13 @@ pub struct Erg {} #[derive(Debug, Clone, Copy)] pub struct Usd {} +#[derive(Debug, Clone, Copy)] +pub struct Btc {} + impl Asset for Erg {} impl Asset for NanoErg {} impl Asset for Usd {} +impl Asset for Btc {} impl Erg { pub fn to_nanoerg(erg: f64) -> f64 { @@ -32,3 +36,15 @@ pub struct AssetsExchangeRate { pub get: GET, pub rate: f64, } + +// Calculates an Exchange Rate of GET/PER2 based on GET/PER1 and PER1/PER2 +pub fn convert_rate( + a: AssetsExchangeRate, + b: AssetsExchangeRate, +) -> AssetsExchangeRate { + AssetsExchangeRate { + per1: b.per1, + get: a.get, + rate: a.rate * b.rate, + } +} diff --git a/core/src/datapoint_source/bitpanda.rs b/core/src/datapoint_source/bitpanda.rs index 60779bff..ec6c6074 100644 --- a/core/src/datapoint_source/bitpanda.rs +++ b/core/src/datapoint_source/bitpanda.rs @@ -1,4 +1,5 @@ use super::assets_exchange_rate::AssetsExchangeRate; +use super::assets_exchange_rate::Btc; use super::assets_exchange_rate::Usd; use super::erg_xau::KgAu; use super::DataPointSourceError; @@ -33,6 +34,33 @@ pub async fn get_kgau_usd() -> Result, DataPointSo } } +// Get USD/BTC. Can be used as a redundant source for ERG/BTC through ERG/USD and USD/BTC +pub(crate) async fn get_btc_usd() -> Result, DataPointSourceError> { + let url = "https://api.bitpanda.com/v1/ticker"; + let resp = reqwest::get(url).await?; + let json = json::parse(&resp.text().await?)?; + if let Some(p) = json["BTC"]["USD"].as_str() { + // USD price of BTC + let usd_per_btc = p + .parse::() + .map_err(|_| DataPointSourceError::JsonMissingField { + field: "BTC.USD as f64".to_string(), + json: json.dump(), + })?; + let rate = AssetsExchangeRate { + per1: Btc {}, + get: Usd {}, + rate: usd_per_btc, + }; + Ok(rate) + } else { + Err(DataPointSourceError::JsonMissingField { + field: "BTC.USD".to_string(), + json: json.dump(), + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -42,4 +70,9 @@ mod tests { let pair: AssetsExchangeRate = tokio_test::block_on(get_kgau_usd()).unwrap(); assert!(pair.rate > 0.0); } + #[test] + fn test_btc_usd_price() { + let pair: AssetsExchangeRate = tokio_test::block_on(get_btc_usd()).unwrap(); + assert!(pair.rate > 0.0); + } } diff --git a/core/src/datapoint_source/coincap.rs b/core/src/datapoint_source/coincap.rs index f5d244ed..ec47a6f3 100644 --- a/core/src/datapoint_source/coincap.rs +++ b/core/src/datapoint_source/coincap.rs @@ -1,4 +1,5 @@ use super::assets_exchange_rate::AssetsExchangeRate; +use super::assets_exchange_rate::Btc; use super::assets_exchange_rate::NanoErg; use super::assets_exchange_rate::Usd; use super::DataPointSourceError; @@ -33,8 +34,36 @@ pub async fn get_usd_nanoerg() -> Result, DataP } } +// Get USD/BTC. Can be used as a redundant source for ERG/BTC through ERG/USD and USD/BTC +pub async fn get_btc_usd() -> Result, DataPointSourceError> { + // see https://coincap.io/assets/ergo + let url = "https://api.coincap.io/v2/assets/bitcoin"; + let resp = reqwest::get(url).await?; + let price_json = json::parse(&resp.text().await?)?; + if let Some(p) = price_json["data"]["priceUsd"].as_str() { + let usd_per_btc = p + .parse::() + .map_err(|_| DataPointSourceError::JsonMissingField { + field: "data.priceUsd as f64".to_string(), + json: price_json.dump(), + })?; + let rate = AssetsExchangeRate { + per1: Btc {}, + get: Usd {}, + rate: usd_per_btc, + }; + Ok(rate) + } else { + Err(DataPointSourceError::JsonMissingField { + field: "btc.priceUsd as string".to_string(), + json: price_json.dump(), + }) + } +} + #[cfg(test)] mod tests { + use super::super::bitpanda; use super::super::coingecko; use super::*; @@ -49,4 +78,16 @@ mod tests { "up to 5% deviation is allowed" ); } + #[test] + fn test_usd_btc_price() { + let pair = tokio_test::block_on(get_btc_usd()).unwrap(); + let bitpanda = tokio_test::block_on(bitpanda::get_btc_usd()).unwrap(); + assert!(pair.rate > 0.0); + dbg!(pair, bitpanda); + let deviation_from_bitpanda = (pair.rate - bitpanda.rate).abs() / bitpanda.rate; + assert!( + deviation_from_bitpanda < 0.05, + "up to 5% deviation is allowed" + ); + } } diff --git a/core/src/datapoint_source/coingecko.rs b/core/src/datapoint_source/coingecko.rs index 39f0bc0d..33dca31e 100644 --- a/core/src/datapoint_source/coingecko.rs +++ b/core/src/datapoint_source/coingecko.rs @@ -3,6 +3,7 @@ use crate::datapoint_source::assets_exchange_rate::NanoErg; use crate::datapoint_source::DataPointSourceError; use super::ada_usd::Lovelace; +use super::assets_exchange_rate::Btc; use super::assets_exchange_rate::Usd; use super::erg_xau::KgAu; @@ -70,6 +71,27 @@ pub async fn get_usd_lovelace() -> Result, Dat } } +pub async fn get_btc_nanoerg() -> Result, DataPointSourceError> { + let url = "https://api.coingecko.com/api/v3/simple/price?ids=ergo&vs_currencies=BTC"; + let resp = reqwest::get(url).await?; + let price_json = json::parse(&resp.text().await?)?; + if let Some(p) = price_json["ergo"]["btc"].as_f64() { + // Convert from price BTC/ERG to nanoERG/BTC + let erg_per_usd = NanoErg::from_erg(1.0 / p); + let rate = AssetsExchangeRate { + per1: Btc {}, + get: NanoErg {}, + rate: erg_per_usd, + }; + Ok(rate) + } else { + Err(DataPointSourceError::JsonMissingField { + field: "ergo.btc as f64".to_string(), + json: price_json.dump(), + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -94,4 +116,10 @@ mod tests { tokio_test::block_on(get_usd_lovelace()).unwrap(); assert!(pair.rate > 0.0); } + #[test] + fn test_erg_btc_price() { + let pair: AssetsExchangeRate = + tokio_test::block_on(get_btc_nanoerg()).unwrap(); + assert!(pair.rate > 0.0); + } } diff --git a/core/src/datapoint_source/erg_btc.rs b/core/src/datapoint_source/erg_btc.rs new file mode 100644 index 00000000..a708e660 --- /dev/null +++ b/core/src/datapoint_source/erg_btc.rs @@ -0,0 +1,65 @@ +use std::pin::Pin; + +use futures::Future; + +use super::{ + assets_exchange_rate::{convert_rate, AssetsExchangeRate, Btc, NanoErg}, + bitpanda, coincap, coingecko, DataPointSourceError, +}; + +#[allow(clippy::type_complexity)] +pub fn nanoerg_btc_sources() -> Vec< + Pin, DataPointSourceError>>>>, +> { + vec![ + Box::pin(coingecko::get_btc_nanoerg()), + Box::pin(get_btc_nanoerg_coincap()), + Box::pin(get_btc_nanoerg_bitpanda()), + ] +} + +// Calculate ERG/BTC through ERG/USD and USD/BTC +async fn get_btc_nanoerg_coincap() -> Result, DataPointSourceError> +{ + Ok(convert_rate( + coincap::get_usd_nanoerg().await?, + coincap::get_btc_usd().await?, + )) +} + +async fn get_btc_nanoerg_bitpanda() -> Result, DataPointSourceError> +{ + Ok(convert_rate( + coincap::get_usd_nanoerg().await?, + bitpanda::get_btc_usd().await?, + )) +} + +#[cfg(test)] +mod test { + use super::coingecko; + use super::get_btc_nanoerg_bitpanda; + use super::get_btc_nanoerg_coincap; + #[test] + fn test_btc_nanoerg_combined() { + let combined = tokio_test::block_on(get_btc_nanoerg_coincap()).unwrap(); + let coingecko = tokio_test::block_on(coingecko::get_btc_nanoerg()).unwrap(); + let bitpanda = tokio_test::block_on(get_btc_nanoerg_bitpanda()).unwrap(); + let deviation_from_coingecko = (combined.rate - coingecko.rate).abs() / coingecko.rate; + assert!( + deviation_from_coingecko < 0.05, + "up to 5% deviation is allowed" + ); + let bitpanda_deviation_from_coingecko = + (bitpanda.rate - coingecko.rate).abs() / coingecko.rate; + assert!( + bitpanda_deviation_from_coingecko < 0.05, + "up to 5% deviation is allowed" + ); + let deviation_from_bitpanda = (bitpanda.rate - combined.rate).abs() / combined.rate; + assert!( + deviation_from_bitpanda < 0.05, + "up to 5% deviation is allowed" + ); + } +} diff --git a/core/src/datapoint_source/erg_xau.rs b/core/src/datapoint_source/erg_xau.rs index e984bce3..b0b680f2 100644 --- a/core/src/datapoint_source/erg_xau.rs +++ b/core/src/datapoint_source/erg_xau.rs @@ -5,6 +5,7 @@ use std::pin::Pin; use futures::Future; use super::aggregator::fetch_aggregated; +use super::assets_exchange_rate::convert_rate; use super::assets_exchange_rate::Asset; use super::assets_exchange_rate::AssetsExchangeRate; use super::assets_exchange_rate::NanoErg; @@ -48,12 +49,7 @@ pub async fn combined_kgau_nanoerg( ) -> Result, DataPointSourceError> { let kgau_usd_rate = bitpanda::get_kgau_usd().await?; let aggregated_usd_nanoerg_rate = fetch_aggregated(nanoerg_usd_sources()).await?; - let rate = kgau_usd_rate.rate * aggregated_usd_nanoerg_rate.rate; - Ok(AssetsExchangeRate { - per1: KgAu {}, - get: NanoErg {}, - rate, - }) + Ok(convert_rate(aggregated_usd_nanoerg_rate, kgau_usd_rate)) } #[cfg(test)] diff --git a/core/src/datapoint_source/predef.rs b/core/src/datapoint_source/predef.rs index 55e70722..3df7992e 100644 --- a/core/src/datapoint_source/predef.rs +++ b/core/src/datapoint_source/predef.rs @@ -2,6 +2,7 @@ use crate::oracle_types::Rate; use super::ada_usd::usd_lovelace_sources; use super::aggregator::fetch_aggregated; +use super::erg_btc::nanoerg_btc_sources; use super::erg_usd::nanoerg_usd_sources; use super::erg_xau::nanoerg_kgau_sources; use super::DataPointSourceError; @@ -28,6 +29,9 @@ async fn fetch_predef_source_aggregated( PredefinedDataPointSource::NanoAdaUsd => { fetch_aggregated(usd_lovelace_sources()).await?.rate } + PredefinedDataPointSource::NanoErgBTC => { + fetch_aggregated(nanoerg_btc_sources()).await?.rate + } }; Ok((rate_float as i64).into()) } diff --git a/core/src/pool_config.rs b/core/src/pool_config.rs index 737b809c..2d7eac9d 100644 --- a/core/src/pool_config.rs +++ b/core/src/pool_config.rs @@ -56,6 +56,7 @@ pub enum PredefinedDataPointSource { NanoErgUsd, NanoErgXau, NanoAdaUsd, + NanoErgBTC, } /// Holds the token ids of every important token used by the oracle pool.