Skip to content

Commit

Permalink
Merge pull request #515 from gregdhill/feat/oracle-xlsd
Browse files Browse the repository at this point in the history
feat: consume dia fair price feed
  • Loading branch information
gregdhill authored Aug 30, 2023
2 parents 0165436 + baa211b commit 79e1235
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 38 deletions.
26 changes: 22 additions & 4 deletions oracle/examples/kintsugi-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@
],
"dia": [
[
"USD",
"BTC=Bitcoin/0x0000000000000000000000000000000000000000"
"BTC=Bitcoin/0x0000000000000000000000000000000000000000",
"USD"
],
[
"USD",
"KSM=Kusama/0x0000000000000000000000000000000000000000"
"KSM=Kusama/0x0000000000000000000000000000000000000000",
"USD"
]
]
}
Expand All @@ -113,6 +113,24 @@
]
]
}
},
{
"pair": [
"BTC",
"VKSM"
],
"feeds": {
"dia_fair_price": [
[
"BTC=KBTC",
"USD"
],
[
"VKSM",
"USD"
]
]
}
}
]
}
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
68 changes: 43 additions & 25 deletions oracle/src/currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,52 @@ pub trait CurrencyInfo<Currency> {
fn decimals(&self, id: &Currency) -> Option<u32>;
}

#[derive(Default, Debug, Clone, Eq, PartialOrd, Ord)]
pub struct Currency {
symbol: String,
path: Option<String>,
#[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),
}

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")),
}
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(),
}
}

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

Expand Down Expand Up @@ -131,7 +143,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 Expand Up @@ -270,6 +282,12 @@ mod tests {

// BTC/USD * DOT/BTC = USD/DOT
assert_reduce!(("BTC" / "USD" @ 19184.24) * ("DOT" / "BTC" @ 0.00032457) = ("USD" / "DOT" @ 0.16060054900429147));

// BTC/USD * KSM/USD = BTC/KSM
assert_reduce!(("BTC" / "USD" @ 27356.159557758947) * ("KSM" / "USD" @ 19.743996225593296) = ("BTC" / "KSM" @ 1385.5431922286498));

// USD/BTC * USD/KSM = BTC/KSM
assert_reduce!(("USD" / "BTC" @ 3.655107115877481e-5) * ("USD" / "KSM" @ 0.05052177613811538) = ("BTC" / "KSM" @ 1382.2242286321239));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion oracle/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ 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("ReqwestError: {0}")]
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
11 changes: 4 additions & 7 deletions oracle/src/feeds/dia.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ impl Default for DiaApi {
}

fn extract_response(value: Value) -> Option<f64> {
value
.get("Price")?
.as_f64()
.and_then(|x| if x.is_normal() { Some(1.0 / x) } else { None })
value.get("Price")?.as_f64()
}

fn set_token_path(base: &mut Url, token_path: &str) {
Expand All @@ -52,10 +49,10 @@ impl DiaApi {
currency_pair: CurrencyPair<Currency>,
_currency_store: &CurrencyStore<String>,
) -> Result<CurrencyPairAndPrice<Currency>, Error> {
if currency_pair.base.symbol() != "USD" {
if currency_pair.quote.symbol() != "USD" {
return Err(Error::InvalidDiaSymbol);
}
let token_path = currency_pair.quote.path().ok_or(Error::InvalidDiaSymbol)?;
let token_path = currency_pair.base.path().ok_or(Error::InvalidDiaSymbol)?;

// https://docs.diadata.org/documentation/api-1/api-endpoints#asset-quotation
let mut url = self.url.clone();
Expand Down Expand Up @@ -115,7 +112,7 @@ mod tests {
"Time": "2022-10-21T07:35:24Z",
"Source": "diadata.org"
})),
Some(1.0 / 5.842649511778436)
Some(5.842649511778436)
)
}
}
112 changes: 112 additions & 0 deletions oracle/src/feeds/dia_fair_price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#![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) -> Option<f64> {
value
.as_array()?
.into_iter()
.find(|entry| matches!(entry.get("Token").and_then(|value| value.as_str()), Some(token) if token.to_uppercase() == alias))?
.get("FairPrice")?
.as_f64()
}

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.quote.symbol() != "USD" {
return Err(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 = currency_pair.base.path().unwrap_or(currency_pair.base.symbol());

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

let price = extract_response(data, alias.as_str()).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",
),
Some(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 79e1235

Please sign in to comment.