Skip to content

Commit

Permalink
Merge pull request #34 from pendulum-chain/32-add-custom-handling-for…
Browse files Browse the repository at this point in the history
…-argentinian-blue-dollar

Add custom handling for argentinian blue dollar
  • Loading branch information
ebma authored Oct 9, 2024
2 parents 445a284 + 1e8a9c7 commit 62d3dac
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 84 deletions.
36 changes: 27 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions dia-batching-server/src/api/binance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use crate::api::error::BinanceError;
use crate::types::{AssetSpecifier, Quotation};
use chrono::Utc;
use rust_decimal::prelude::Zero;
use rust_decimal::Decimal;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

pub struct BinancePriceApi {
client: BinanceClient,
}

impl BinancePriceApi {
pub fn new() -> Self {
let client: BinanceClient = BinanceClient::default();

Self { client }
}

pub async fn get_price(&self, asset: &AssetSpecifier) -> Result<Quotation, BinanceError> {
let binance_asset_identifier = Self::convert_to_binance_id(&asset);

match self.client.price(binance_asset_identifier).await {
Ok(price) => Ok(Quotation {
symbol: asset.symbol.clone(),
name: asset.symbol.clone(),
blockchain: Some(asset.blockchain.clone().into()),
price: price.price,
supply: Decimal::zero(),
time: Utc::now().timestamp().unsigned_abs(),
}),
Err(error) => {
log::warn!("Error getting price for {:?} from Binance: {:?}", asset, error);
Err(error)
},
}
}

// We assume here that the `symbol` field of the `AssetSpecifier` is a valid asset for Binance.
// The `blockchain` field is not used and ignored.
fn convert_to_binance_id(asset: &AssetSpecifier) -> String {
asset.symbol.to_uppercase()
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BinancePrice {
pub symbol: String,
pub price: Decimal,
}

pub struct BinanceClient {
host: String,
inner: reqwest::Client,
}

impl BinanceClient {
pub fn default() -> Self {
Self::new("https://api.binance.com".to_string())
}

pub fn new(host: String) -> Self {
let inner = reqwest::Client::new();

Self { host, inner }
}

async fn get<R: DeserializeOwned>(&self, endpoint: &str) -> Result<R, BinanceError> {
let url = reqwest::Url::parse(
format!("{host}/{ep}", host = self.host.as_str(), ep = endpoint).as_str(),
)
.expect("Invalid URL");

let response = self
.inner
.get(url)
.send()
.await
.map_err(|e| BinanceError(format!("Failed to send request: {}", e.to_string())))?;

if !response.status().is_success() {
let result = response.text().await;
return Err(BinanceError(format!(
"Binance API error: {}",
result.unwrap_or("Unknown".to_string()).trim()
)));
}

let result = response.json().await;
result.map_err(|e| BinanceError(format!("Could not decode Binance response: {}", e)))
}

pub async fn price(&self, symbol: String) -> Result<BinancePrice, BinanceError> {
let endpoint = format!("api/v3/ticker/price?symbol={}", symbol);
let response: BinancePrice = self.get(&endpoint).await?;
Ok(response)
}
}

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

#[tokio::test]
async fn test_fetching_single_price() {
let client = BinanceClient::default();

let id = "USDTARS";

let price = client.price(id.to_string()).await.expect("Should return a price");

assert_eq!(price.symbol, id);
assert!(price.price > 0.into());
}
}
7 changes: 3 additions & 4 deletions dia-batching-server/src/api/coingecko.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,9 @@ mod tests {

// Check if all assets have a quotation and if not, print the missing ones
for asset in assets {
let quotation = quotations
.iter()
.find(|q| q.symbol == asset.symbol)
.expect(format!("Could not find a quotation for asset specifier {:?}", asset).as_str());
let quotation = quotations.iter().find(|q| q.symbol == asset.symbol).expect(
format!("Could not find a quotation for asset specifier {:?}", asset).as_str(),
);
assert_eq!(quotation.symbol, asset.symbol);
assert_eq!(quotation.name, asset.symbol);
assert_eq!(quotation.blockchain, Some(asset.blockchain.clone()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,16 @@
use crate::api::custom::AssetCompatibility;
use crate::api::error::CustomError;
use crate::api::Quotation;
use crate::AssetSpecifier;
use crate::types::{AssetSpecifier, Quotation};
use async_trait::async_trait;
use chrono::prelude::*;
use chrono::Utc;
use graphql_client::{GraphQLQuery, Response};
use rust_decimal::prelude::Zero;
use rust_decimal::Decimal;
use std::string::ToString;

pub struct CustomPriceApi;

impl CustomPriceApi {
pub async fn get_price(asset: &AssetSpecifier) -> Result<Quotation, CustomError> {
let api =
Self::get_supported_api(asset).ok_or(CustomError("Unsupported asset".to_string()))?;
api.get_price(asset).await
}

pub fn is_supported(asset: &AssetSpecifier) -> bool {
Self::get_supported_api(asset).is_some()
}

/// Iterates over all supported APIs and returns the first one that supports the given asset.
fn get_supported_api(asset: &AssetSpecifier) -> Option<Box<dyn AssetCompatibility>> {
let compatible_apis: Vec<Box<dyn AssetCompatibility>> = vec![Box::new(AmpePriceView)];

for api in compatible_apis {
if api.supports(asset) {
return Some(api);
}
}

None
}
}

#[async_trait]
trait AssetCompatibility: Send {
fn supports(&self, asset: &AssetSpecifier) -> bool;

async fn get_price(&self, asset: &AssetSpecifier) -> Result<Quotation, CustomError>;
}
// The blockchain and symbol for the Amplitude native token
// These are the expected values for the asset specifier.
const BLOCKCHAIN: &'static str = "Amplitude";
const SYMBOL: &'static str = "AMPE";

// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
Expand All @@ -54,7 +25,7 @@ pub struct AmpePriceView;
#[async_trait]
impl AssetCompatibility for AmpePriceView {
fn supports(&self, asset: &AssetSpecifier) -> bool {
asset.blockchain.to_uppercase() == "AMPLITUDE" && asset.symbol.to_uppercase() == "AMPE"
asset.blockchain.to_uppercase() == BLOCKCHAIN && asset.symbol.to_uppercase() == SYMBOL
}

async fn get_price(&self, _asset: &AssetSpecifier) -> Result<Quotation, CustomError> {
Expand All @@ -63,8 +34,6 @@ impl AssetCompatibility for AmpePriceView {
}

impl AmpePriceView {
const SYMBOL: &'static str = "AMPE";
const BLOCKCHAIN: &'static str = "Amplitude";
const URL: &'static str = "https://squid.subsquid.io/amplitude-squid/graphql";

/// Response:
Expand Down Expand Up @@ -102,19 +71,19 @@ impl AmpePriceView {
let price = response_data.bundle_by_id.eth_price;

Ok(Quotation {
symbol: Self::SYMBOL.to_string(),
name: Self::SYMBOL.to_string(),
blockchain: Some(Self::BLOCKCHAIN.to_string()),
symbol: SYMBOL.to_string(),
name: SYMBOL.to_string(),
blockchain: Some(BLOCKCHAIN.to_string()),
price,
supply: Decimal::from(0),
supply: Decimal::zero(),
time: Utc::now().timestamp().unsigned_abs(),
})
}
}

#[cfg(test)]
mod tests {
use crate::api::custom::AmpePriceView;
use super::{AmpePriceView, BLOCKCHAIN, SYMBOL};
use crate::api::custom::CustomPriceApi;
use crate::AssetSpecifier;

Expand All @@ -123,8 +92,10 @@ mod tests {
let asset =
AssetSpecifier { blockchain: "Amplitude".to_string(), symbol: "AMPE".to_string() };

let ampe_quotation =
CustomPriceApi::get_price(&asset).await.expect("should return a quotation");
let ampe_quotation = CustomPriceApi::new()
.get_price(&asset)
.await
.expect("should return a quotation");

assert_eq!(ampe_quotation.symbol, asset.symbol);
assert_eq!(ampe_quotation.name, asset.symbol);
Expand All @@ -136,12 +107,9 @@ mod tests {
async fn test_get_ampe_price_from_view() {
let ampe_quotation = AmpePriceView::get_price().await.expect("should return a quotation");

assert_eq!(ampe_quotation.symbol, AmpePriceView::SYMBOL);
assert_eq!(ampe_quotation.name, AmpePriceView::SYMBOL);
assert_eq!(
ampe_quotation.blockchain.expect("should return something"),
AmpePriceView::BLOCKCHAIN
);
assert_eq!(ampe_quotation.symbol, SYMBOL);
assert_eq!(ampe_quotation.name, SYMBOL);
assert_eq!(ampe_quotation.blockchain.expect("should return something"), BLOCKCHAIN);
assert!(ampe_quotation.price > 0.into());
}
}
Loading

0 comments on commit 62d3dac

Please sign in to comment.