diff --git a/core/src/datapoint_source.rs b/core/src/datapoint_source.rs index 9802a7ca..20c3a108 100644 --- a/core/src/datapoint_source.rs +++ b/core/src/datapoint_source.rs @@ -8,8 +8,11 @@ mod coingecko; mod custom_ext_script; mod erg_btc; mod erg_usd; +mod erg_xag; mod erg_xau; +mod ergodex; mod predef; +mod rsn_xag; use crate::oracle_types::Rate; use crate::pool_config::PredefinedDataPointSource; diff --git a/core/src/datapoint_source/bitpanda.rs b/core/src/datapoint_source/bitpanda.rs index 379e2d07..af57a2cb 100644 --- a/core/src/datapoint_source/bitpanda.rs +++ b/core/src/datapoint_source/bitpanda.rs @@ -1,6 +1,7 @@ use super::assets_exchange_rate::AssetsExchangeRate; use super::assets_exchange_rate::Btc; use super::assets_exchange_rate::Usd; +use super::erg_xag::KgAg; use super::erg_xau::KgAu; use super::DataPointSourceError; @@ -48,6 +49,47 @@ pub async fn get_kgau_usd() -> Result, DataPointSo Ok(rate) } +#[cfg(not(test))] +pub async fn get_kgag_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["XAG"]["USD"].as_str() { + // USD price of 1 gram of silver + let p_float = p + .parse::() + .map_err(|_| DataPointSourceError::JsonMissingField { + field: "XAG.USD as f64".to_string(), + json: json.dump(), + })?; + let usd_per_kgag = KgAg::from_gram(p_float); + let rate = AssetsExchangeRate { + per1: KgAg {}, + get: Usd {}, + rate: usd_per_kgag, + }; + Ok(rate) + } else { + Err(DataPointSourceError::JsonMissingField { + field: "XAG.USD".to_string(), + json: json.dump(), + }) + } +} + +#[cfg(test)] +pub async fn get_kgag_usd() -> Result, DataPointSourceError> { + // USD price of 1 gram of silver + let p_float = 0.765; + let usd_per_kgag = KgAg::from_gram(p_float); + let rate = AssetsExchangeRate { + per1: KgAg {}, + get: Usd {}, + rate: usd_per_kgag, + }; + Ok(rate) +} + #[cfg(not(test))] // 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> { @@ -97,6 +139,13 @@ mod tests { let pair: AssetsExchangeRate = tokio_test::block_on(get_kgau_usd()).unwrap(); assert!(pair.rate > 0.0); } + + #[test] + fn test_kgag_usd_price() { + let pair: AssetsExchangeRate = tokio_test::block_on(get_kgag_usd()).unwrap(); + assert!(pair.rate > 0.0); + } + #[test] fn test_btc_usd_price() { let pair: AssetsExchangeRate = tokio_test::block_on(get_btc_usd()).unwrap(); diff --git a/core/src/datapoint_source/coingecko.rs b/core/src/datapoint_source/coingecko.rs index 1022fe4e..1dc3ab17 100644 --- a/core/src/datapoint_source/coingecko.rs +++ b/core/src/datapoint_source/coingecko.rs @@ -1,11 +1,12 @@ -use crate::datapoint_source::assets_exchange_rate::AssetsExchangeRate; -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_xag::KgAg; use super::erg_xau::KgAu; +use crate::datapoint_source::assets_exchange_rate::AssetsExchangeRate; +use crate::datapoint_source::assets_exchange_rate::NanoErg; +use crate::datapoint_source::rsn_xag::Rsn; +use crate::datapoint_source::DataPointSourceError; #[cfg(not(test))] pub async fn get_kgau_nanoerg() -> Result, DataPointSourceError> { @@ -32,7 +33,7 @@ pub async fn get_kgau_nanoerg() -> Result, Dat #[cfg(test)] pub async fn get_kgau_nanoerg() -> Result, DataPointSourceError> { - let nanoerg_per_troy_ounce = NanoErg::from_erg(1.0 / 0.0008162); + let nanoerg_per_troy_ounce = NanoErg::from_erg(1.0 / 0.0482); let nanoerg_per_kg = KgAu::from_troy_ounce(nanoerg_per_troy_ounce); let rate = AssetsExchangeRate { per1: KgAu {}, @@ -42,6 +43,41 @@ pub async fn get_kgau_nanoerg() -> Result, Dat Ok(rate) } +#[cfg(not(test))] +pub async fn get_kgag_nanoerg() -> Result, DataPointSourceError> { + let url = "https://api.coingecko.com/api/v3/simple/price?ids=ergo&vs_currencies=XAG"; + let resp = reqwest::get(url).await?; + let price_json = json::parse(&resp.text().await?)?; + if let Some(p) = price_json["ergo"]["xag"].as_f64() { + // Convert from price Erg/XAG to nanoErgs per 1 XAG + let nanoerg_per_troy_ounce = NanoErg::from_erg(1.0 / p); + let nanoerg_per_kg = KgAg::from_troy_ounce(nanoerg_per_troy_ounce); + let rate = AssetsExchangeRate { + per1: KgAg {}, + get: NanoErg {}, + rate: nanoerg_per_kg, + }; + Ok(rate) + } else { + Err(DataPointSourceError::JsonMissingField { + field: "ergo.xag as f64".to_string(), + json: price_json.dump(), + }) + } +} + +#[cfg(test)] +pub async fn get_kgag_nanoerg() -> Result, DataPointSourceError> { + let nanoerg_per_troy_ounce = NanoErg::from_erg(1.0 / 0.0706); + let nanoerg_per_kg = KgAg::from_troy_ounce(nanoerg_per_troy_ounce); + let rate = AssetsExchangeRate { + per1: KgAg {}, + get: NanoErg {}, + rate: nanoerg_per_kg, + }; + Ok(rate) +} + #[cfg(not(test))] pub async fn get_usd_nanoerg() -> Result, DataPointSourceError> { let url = "https://api.coingecko.com/api/v3/simple/price?ids=ergo&vs_currencies=USD"; @@ -144,6 +180,46 @@ pub async fn get_btc_nanoerg() -> Result, DataP Ok(rate) } +pub async fn get_kgag_rsn() -> Result, DataPointSourceError> { + let url = "https://api.coingecko.com/api/v3/simple/price?ids=rosen-bridge&vs_currencies=XAG"; + let resp = reqwest::get(url).await?; + let price_json = json::parse(&resp.text().await?)?; + if let Some(p) = price_json["rosen-bridge"]["xag"].as_f64() { + // Convert from price RSN/XAG + let rsn_per_ag = KgAg::from_troy_ounce(1.0 / p); + let rate = AssetsExchangeRate { + per1: KgAg {}, + get: Rsn {}, + rate: rsn_per_ag, + }; + Ok(rate) + } else { + Err(DataPointSourceError::JsonMissingField { + field: "rsn.xag as f64".to_string(), + json: price_json.dump(), + }) + } +} + +pub async fn get_rsn_usd() -> Result, DataPointSourceError> { + let url = "https://api.coingecko.com/api/v3/simple/price?ids=rosen-bridge&vs_currencies=USD"; + let resp = reqwest::get(url).await?; + let price_json = json::parse(&resp.text().await?)?; + if let Some(p) = price_json["rosen-bridge"]["usd"].as_f64() { + let rate = AssetsExchangeRate { + per1: Usd {}, + get: Rsn {}, + rate: 1.0 / p, + }; + Ok(rate) + } else { + Err(DataPointSourceError::JsonMissingField { + field: "rsn.usd as f64".to_string(), + json: price_json.dump(), + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -155,6 +231,13 @@ mod tests { assert!(pair.rate > 0.0); } + #[test] + fn test_erg_xag_price() { + let pair: AssetsExchangeRate = + tokio_test::block_on(get_kgag_nanoerg()).unwrap(); + assert!(pair.rate > 0.0); + } + #[test] fn test_erg_usd_price() { let pair: AssetsExchangeRate = @@ -174,4 +257,16 @@ mod tests { tokio_test::block_on(get_btc_nanoerg()).unwrap(); assert!(pair.rate > 0.0); } + + #[test] + fn test_rsn_xag_price() { + let pair: AssetsExchangeRate = tokio_test::block_on(get_kgag_rsn()).unwrap(); + assert!(pair.rate > 0.0); + } + + #[test] + fn test_rsn_usd_price() { + let pair: AssetsExchangeRate = tokio_test::block_on(get_rsn_usd()).unwrap(); + assert!(pair.rate > 0.0); + } } diff --git a/core/src/datapoint_source/erg_xag.rs b/core/src/datapoint_source/erg_xag.rs new file mode 100644 index 00000000..57eccfa6 --- /dev/null +++ b/core/src/datapoint_source/erg_xag.rs @@ -0,0 +1,67 @@ +//! Obtains the nanoErg per 1 XAG (troy ounce of silver) rate + +use std::pin::Pin; + +use futures::Future; + +use crate::datapoint_source::aggregator::fetch_aggregated; +use crate::datapoint_source::assets_exchange_rate::{ + convert_rate, Asset, AssetsExchangeRate, NanoErg, +}; +use crate::datapoint_source::erg_usd::nanoerg_usd_sources; +use crate::datapoint_source::{bitpanda, coingecko, DataPointSourceError}; + +#[derive(Debug, Clone, Copy)] +pub struct KgAg {} + +#[derive(Debug, Clone, Copy)] +pub struct Xag {} + +impl Asset for KgAg {} + +impl Asset for Xag {} + +impl KgAg { + pub fn from_troy_ounce(oz: f64) -> f64 { + // https://en.wikipedia.org/wiki/Gold_bar + // troy ounces per kg + oz * 32.150746568627 + } + + pub fn from_gram(g: f64) -> f64 { + g * 1000.0 + } +} + +#[allow(clippy::type_complexity)] +pub fn nanoerg_kgag_sources() -> Vec< + Pin, DataPointSourceError>>>>, +> { + vec![ + Box::pin(coingecko::get_kgag_nanoerg()), + Box::pin(combined_kgag_nanoerg()), + ] +} + +pub async fn combined_kgag_nanoerg( +) -> Result, DataPointSourceError> { + let kgag_usd_rate = bitpanda::get_kgag_usd().await?; + let aggregated_usd_nanoerg_rate = fetch_aggregated(nanoerg_usd_sources()).await?; + Ok(convert_rate(aggregated_usd_nanoerg_rate, kgag_usd_rate)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_kgag_nanoerg_combined() { + let combined = tokio_test::block_on(combined_kgag_nanoerg()).unwrap(); + let coingecko = tokio_test::block_on(coingecko::get_kgag_nanoerg()).unwrap(); + let deviation_from_coingecko = (combined.rate - coingecko.rate).abs() / coingecko.rate; + assert!( + deviation_from_coingecko < 0.05, + "up to 5% deviation is allowed" + ); + } +} diff --git a/core/src/datapoint_source/ergodex.rs b/core/src/datapoint_source/ergodex.rs new file mode 100644 index 00000000..d0d2a08f --- /dev/null +++ b/core/src/datapoint_source/ergodex.rs @@ -0,0 +1,41 @@ +use crate::datapoint_source::assets_exchange_rate::{AssetsExchangeRate, NanoErg}; +use crate::datapoint_source::rsn_xag::Rsn; +use crate::datapoint_source::DataPointSourceError; + +pub async fn get_rsn_nanoerg() -> Result, DataPointSourceError> { + let url = "https://api.spectrum.fi/v1/amm/pool/1b694b15467c62f0cd4525e368dbdea2329c713aa200b73df4a622e950551b40/stats"; + let resp = reqwest::get(url).await?; + let pool_json = json::parse(&resp.text().await?)?; + let locked_erg = pool_json["lockedX"]["amount"].as_f64().ok_or_else(|| { + DataPointSourceError::JsonMissingField { + field: "lockedX.amount as f64".to_string(), + json: pool_json.dump(), + } + })?; + + let locked_rsn = pool_json["lockedY"]["amount"].as_f64().ok_or_else(|| { + DataPointSourceError::JsonMissingField { + field: "lockedY.amount as f64".to_string(), + json: pool_json.dump(), + } + })?; + let price = Rsn::from_rsn(Rsn::from_rsn(locked_rsn) / NanoErg::from_erg(locked_erg)); + let rate = AssetsExchangeRate { + per1: NanoErg {}, + get: Rsn {}, + rate: price, + }; + Ok(rate) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rsn_nanoerg_price() { + let pair: AssetsExchangeRate = + tokio_test::block_on(get_rsn_nanoerg()).unwrap(); + assert!(pair.rate > 0.0); + } +} diff --git a/core/src/datapoint_source/predef.rs b/core/src/datapoint_source/predef.rs index 3df7992e..e4602cdb 100644 --- a/core/src/datapoint_source/predef.rs +++ b/core/src/datapoint_source/predef.rs @@ -1,3 +1,4 @@ +use crate::datapoint_source::rsn_xag::rsn_kgag_sources; use crate::oracle_types::Rate; use super::ada_usd::usd_lovelace_sources; @@ -26,12 +27,16 @@ async fn fetch_predef_source_aggregated( PredefinedDataPointSource::NanoErgXau => { fetch_aggregated(nanoerg_kgau_sources()).await?.rate } + PredefinedDataPointSource::NanoErgXag => { + fetch_aggregated(nanoerg_kgau_sources()).await?.rate + } PredefinedDataPointSource::NanoAdaUsd => { fetch_aggregated(usd_lovelace_sources()).await?.rate } PredefinedDataPointSource::NanoErgBTC => { fetch_aggregated(nanoerg_btc_sources()).await?.rate } + PredefinedDataPointSource::RsnXag => fetch_aggregated(rsn_kgag_sources()).await?.rate, }; Ok((rate_float as i64).into()) } diff --git a/core/src/datapoint_source/rsn_xag.rs b/core/src/datapoint_source/rsn_xag.rs new file mode 100644 index 00000000..e0380800 --- /dev/null +++ b/core/src/datapoint_source/rsn_xag.rs @@ -0,0 +1,72 @@ +use futures::Future; +use std::pin::Pin; + +use crate::datapoint_source::assets_exchange_rate::{convert_rate, Asset, AssetsExchangeRate}; +use crate::datapoint_source::erg_xag::KgAg; +use crate::datapoint_source::{bitpanda, coingecko, ergodex, DataPointSourceError}; + +#[derive(Debug, Clone, Copy)] +pub struct Rsn {} + +impl Asset for Rsn {} + +impl Rsn { + pub fn from_rsn(rsn: f64) -> f64 { + rsn * 1_000.0 + } +} + +#[allow(clippy::type_complexity)] +pub fn rsn_kgag_sources( +) -> Vec, DataPointSourceError>>>>> +{ + vec![ + Box::pin(coingecko::get_kgag_rsn()), + Box::pin(get_rsn_kgag_erg()), + Box::pin(get_rsn_kgag_usd()), + ] +} + +// Calculate RSN/KGAG through RSN/USD and KGAG/USD +async fn get_rsn_kgag_usd() -> Result, DataPointSourceError> { + Ok(convert_rate( + coingecko::get_rsn_usd().await?, + bitpanda::get_kgag_usd().await?, + )) +} + +// Calculate KGAG/RSN through KGAG/ERG and ERG/RSN +async fn get_rsn_kgag_erg() -> Result, DataPointSourceError> { + Ok(convert_rate( + ergodex::get_rsn_nanoerg().await?, + coingecko::get_kgag_nanoerg().await?, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_kgag_rsn_combined() { + let combined = tokio_test::block_on(get_rsn_kgag_usd()).unwrap(); + let coingecko = tokio_test::block_on(coingecko::get_kgag_rsn()).unwrap(); + let ergodex = tokio_test::block_on(get_rsn_kgag_erg()).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 ergodex_deviation_from_coingecko = + (ergodex.rate - coingecko.rate).abs() / coingecko.rate; + assert!( + ergodex_deviation_from_coingecko < 0.05, + "up to 5% deviation is allowed" + ); + let deviation_from_ergodex = (ergodex.rate - combined.rate).abs() / combined.rate; + assert!( + deviation_from_ergodex < 0.05, + "up to 5% deviation is allowed" + ); + } +} diff --git a/core/src/pool_config.rs b/core/src/pool_config.rs index 33312579..a3a177d0 100644 --- a/core/src/pool_config.rs +++ b/core/src/pool_config.rs @@ -56,8 +56,10 @@ pub struct PoolConfig { pub enum PredefinedDataPointSource { NanoErgUsd, NanoErgXau, + NanoErgXag, NanoAdaUsd, NanoErgBTC, + RsnXag, } /// Holds the token ids of every important token used by the oracle pool.