Skip to content

Commit

Permalink
feat: consume dia fair price feed
Browse files Browse the repository at this point in the history
Signed-off-by: Gregory Hill <[email protected]>
  • Loading branch information
gregdhill committed Aug 29, 2023
1 parent 0165436 commit 8f21f88
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 26 deletions.
2 changes: 1 addition & 1 deletion oracle/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub struct PriceConfig<Currency> {
#[serde(default)]
pub value: Option<f64>,
// Feeds to consume to calculate this exchange rate.
#[serde(default)]
#[serde(default = "BTreeMap::new")]
pub feeds: BTreeMap<FeedName, Vec<CurrencyPair<Currency>>>,
}

Expand Down
84 changes: 60 additions & 24 deletions oracle/src/currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,76 @@ pub trait CurrencyInfo<Currency> {
fn decimals(&self, id: &Currency) -> Option<u32>;
}

#[derive(Default, Debug, Clone, Eq, PartialOrd, Ord)]
pub struct Currency {
#[derive(Deserialize, Debug, Clone)]
pub struct Extension {
pub(crate) alias: Option<String>,
pub(crate) index: Option<usize>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Extended {
symbol: String,
path: Option<String>,
#[serde(flatten)]
pub(crate) ext: Option<Extension>,
}

impl<'de> Deserialize<'de> for Currency {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = String::deserialize(deserializer)?;
match value.split('=').collect::<Vec<_>>()[..] {
[symbol] => Ok(Self {
symbol: symbol.to_string(),
path: None,
}),
[symbol, path] => Ok(Self {
symbol: symbol.to_string(),
path: Some(path.to_string()),
}),
_ => Err(Error::custom("Invalid currency")),
}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Currency {
#[serde(deserialize_with = "deserialize_as_string")]
Symbol(String),
#[serde(deserialize_with = "deserialize_as_tuple")]
Path(String, String),
Extended(Extended),
}

fn deserialize_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = String::deserialize(deserializer)?;
if value.contains('=') {
return Err(Error::custom("Not string"));
}
Ok(value)
}

fn deserialize_as_tuple<'de, D>(deserializer: D) -> Result<(String, String), D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = String::deserialize(deserializer)?;
match value.split('=').collect::<Vec<_>>()[..] {
[symbol, path] => Ok((symbol.to_string(), path.to_string())),
_ => Err(Error::custom("Not tuple")),
}
}

impl Currency {
pub fn symbol(&self) -> String {
self.symbol.to_owned()
match self {
Self::Symbol(symbol) => symbol.to_owned(),
Self::Path(symbol, _) => symbol.to_owned(),
Self::Extended(extended) => extended.symbol.to_owned(),
}
}

pub fn ext(&self) -> Option<Extension> {
match self {
Self::Symbol(_) => None,
Self::Path(_, _) => None,
Self::Extended(extended) => extended.ext.to_owned(),
}
}

pub fn path(&self) -> Option<String> {
self.path.to_owned()
match self {
Self::Symbol(_) => None,
Self::Path(_, path) => Some(path.to_owned()),
Self::Extended(_) => None,
}
}
}

Expand Down Expand Up @@ -131,7 +167,7 @@ impl fmt::Display for CurrencyPairAndPrice<Currency> {
}
}

impl<Currency: Clone + PartialEq + Ord> CurrencyPairAndPrice<Currency> {
impl<Currency: Clone + PartialEq> CurrencyPairAndPrice<Currency> {
pub fn invert(self) -> Self {
Self {
pair: self.pair.invert(),
Expand Down
4 changes: 3 additions & 1 deletion oracle/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ pub enum Error {
InvalidConfig(Box<PriceConfigError<Currency>>),
#[error("{0} not configured")]
NotConfigured(FeedName),
#[error("Invalid dia symbol. Base must be USD & quote must be <symbol>=<id>. E.g. STDOT=Moonbeam/0xFA36Fe1dA08C89eC72Ea1F0143a35bFd5DAea108")]
#[error("Invalid dia symbol")]
InvalidDiaSymbol,
#[error("Index required for fair price feed")]
NoFairPriceIndex,

#[error("ReqwestError: {0}")]
ReqwestError(#[from] ReqwestError),
Expand Down
11 changes: 11 additions & 0 deletions oracle/src/feeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod blockcypher;
mod blockstream;
mod coingecko;
mod dia;
mod dia_fair_price;
mod gateio;
mod kraken;

Expand All @@ -22,6 +23,7 @@ pub use blockcypher::{BlockCypherApi, BlockCypherCli};
pub use blockstream::{BlockstreamApi, BlockstreamCli};
pub use coingecko::{CoinGeckoApi, CoinGeckoCli};
pub use dia::{DiaApi, DiaCli};
pub use dia_fair_price::{DiaFairPriceApi, DiaFairPriceCli};
pub use gateio::{GateIoApi, GateIoCli};
pub use kraken::{KrakenApi, KrakenCli};

Expand All @@ -38,6 +40,8 @@ pub enum FeedName {
GateIo,
CoinGecko,
Dia,
#[serde(rename = "dia_fair_price")]
DiaFairPrice,
}

impl fmt::Display for FeedName {
Expand Down Expand Up @@ -83,6 +87,13 @@ impl PriceFeeds {
}
}

pub fn maybe_add_dia_fair_price(&mut self, opts: DiaFairPriceCli) {
if let Some(api) = DiaFairPriceApi::from_opts(opts) {
log::info!("🔗 DiaFairPrice");
self.feeds.insert(FeedName::DiaFairPrice, Box::new(api));
}
}

pub fn maybe_add_gateio(&mut self, opts: GateIoCli) {
if let Some(api) = GateIoApi::from_opts(opts) {
log::info!("🔗 gate.io");
Expand Down
120 changes: 120 additions & 0 deletions oracle/src/feeds/dia_fair_price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#![allow(clippy::single_char_pattern)]
use super::{get_http, PriceFeed};
use crate::{config::CurrencyStore, currency::*, Error};
use async_trait::async_trait;
use clap::Parser;
use reqwest::Url;
use serde_json::Value;

#[derive(Parser, Debug, Clone)]
pub struct DiaFairPriceCli {
/// Fetch the exchange rate from Dia xLSD feed
#[clap(long)]
dia_fair_price_url: Option<Url>,
}

pub struct DiaFairPriceApi {
url: Url,
}

impl Default for DiaFairPriceApi {
fn default() -> Self {
Self {
url: Url::parse("https://api.diadata.org/xlsd/").unwrap(),
}
}
}

fn extract_response(value: Value, alias: &str, index: usize) -> Option<f64> {
let entry = value.get(index)?;

if matches!(entry.get("Token")?.as_str(), Some(token) if token.to_uppercase() != alias) {
// expected index position does not match token
return None;
}

entry
.get("FairPrice")?
.as_f64()
.and_then(|x| if x.is_normal() { Some(1.0 / x) } else { None })
}

impl DiaFairPriceApi {
pub fn from_opts(opts: DiaFairPriceCli) -> Option<Self> {
opts.dia_fair_price_url.map(Self::new)
}

pub fn new(url: Url) -> Self {
Self { url }
}

async fn get_exchange_rate(
&self,
currency_pair: CurrencyPair<Currency>,
_currency_store: &CurrencyStore<String>,
) -> Result<CurrencyPairAndPrice<Currency>, Error> {
if currency_pair.base.symbol() != "USD" {
return Err(Error::InvalidDiaSymbol);
}
let extension = currency_pair.quote.ext().ok_or(Error::InvalidDiaSymbol)?;
// this allows us to override the expected token name
// which is helpful when using the xlsd feed of a wrapped token
// but we want to submit the currency as the underlying (e.g. KBTC -> BTC)
let alias = extension.alias.unwrap_or(currency_pair.quote.symbol());
let index = extension.index.ok_or(Error::NoFairPriceIndex)?;

let url = self.url.clone();
let data = get_http(url).await?;

let price = extract_response(data, alias.as_str(), index).ok_or(Error::InvalidResponse)?;

Ok(CurrencyPairAndPrice {
pair: currency_pair,
price,
})
}
}

#[async_trait]
impl PriceFeed for DiaFairPriceApi {
async fn get_price(
&self,
currency_pair: CurrencyPair<Currency>,
currency_store: &CurrencyStore<String>,
) -> Result<CurrencyPairAndPrice<Currency>, Error> {
self.get_exchange_rate(currency_pair, currency_store).await
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn should_extract_response() {
assert_eq!(
extract_response(
json!([
{
"Token": "KBTC",
"FairPrice": 27418.406434486784,
"BaseAssetSymbol": "BTC",
"BaseAssetPrice": 27418.406434486784,
"Issuer": "Interlay"
},
{
"Token": "vKSM",
"FairPrice": 24.611983172737727,
"BaseAssetSymbol": "KSM",
"BaseAssetPrice": 19.827745134261495,
"Issuer": "Bifrost"
}
]),
"KBTC",
0
),
Some(1.0 / 27418.406434486784)
)
}
}
5 changes: 5 additions & 0 deletions oracle/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ struct Opts {
#[clap(flatten)]
dia: feeds::DiaCli,

/// Connection settings for DiaFairPrice
#[clap(flatten)]
dia_fair_price: feeds::DiaFairPriceCli,

/// Connection settings for gate.io
#[clap(flatten)]
gateio: feeds::GateIoCli,
Expand Down Expand Up @@ -177,6 +181,7 @@ async fn _main() -> Result<(), Error> {
let mut price_feeds = feeds::PriceFeeds::new(currency_store.clone());
price_feeds.maybe_add_coingecko(opts.coingecko);
price_feeds.maybe_add_dia(opts.dia);
price_feeds.maybe_add_dia_fair_price(opts.dia_fair_price);
price_feeds.maybe_add_gateio(opts.gateio);
price_feeds.maybe_add_kraken(opts.kraken);

Expand Down

0 comments on commit 8f21f88

Please sign in to comment.