Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: consume dia fair price feed #515

Merged
merged 3 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
gregdhill marked this conversation as resolved.
Show resolved Hide resolved
}

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))?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do alias to uppercase as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oracle config already assumes all token symbols are capitalized, if this is not the case then many parts of the code would not work correctly.

.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