Skip to content

Commit

Permalink
Merge pull request #10 from bandprotocol/add-source-coinmarketcap
Browse files Browse the repository at this point in the history
Add source coinmarketcap
  • Loading branch information
RogerKSI authored Jan 31, 2024
2 parents 3aa9ba9 + 182bc42 commit 0e3a441
Show file tree
Hide file tree
Showing 12 changed files with 486 additions and 0 deletions.
1 change: 1 addition & 0 deletions price-adapter-raw/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

- Support CoinGecko sources
- Support Binance Websocket source
- Support CoinMarketCap source
16 changes: 16 additions & 0 deletions price-adapter-raw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ async fn main() {
}
```

### CoinMarketCap

To use CoinMarketCap API, you need to create a `CoinMarketCapPro` instance and set the api key.

```rust
use price_adapter_raw::CoinMarketCap;

#[tokio::main]
async fn main() {
let coingecko = CoinMarketCap::new_with_api_key("$API_KEY".into());
let queries = vec!["ethereum"];
let prices = coingecko.get_prices(&queries).await;
println!("prices: {:?}", prices);
}
```

### Binance Websocket

To use binance websocket API, you need to create a `BinanceWebsocket` instance and set the query symbols.
Expand Down
9 changes: 9 additions & 0 deletions price-adapter-raw/examples/coinmarketcap-pro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use price_adapter_raw::CoinMarketCap;

#[tokio::main]
async fn main() {
let coingecko = CoinMarketCap::new_with_api_key("$API_KEY".into());
let queries = vec!["ethereum"];
let prices = coingecko.get_prices(&queries).await;
println!("prices: {:?}", prices);
}
126 changes: 126 additions & 0 deletions price-adapter-raw/src/coinmarketcap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::{collections::HashMap, vec};

use chrono::{NaiveDateTime, TimeZone, Utc};
use reqwest::{Client, Response, StatusCode};
use serde::Deserialize;

use crate::{error::Error, types::PriceInfo};

const PRO_ENDPOINT: &str = "https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest";

#[derive(Deserialize, Debug, Clone)]
pub struct Currency {
price: f64,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Quote {
#[serde(rename = "USD")]
usd: Currency,
}

#[derive(Deserialize, Debug, Clone)]
pub struct CoinInfo {
pub slug: String,
pub last_updated: String,
pub quote: Quote,
}

#[derive(Deserialize, Debug, Clone)]
pub struct CoinMarketResponse {
data: HashMap<String, CoinInfo>,
}

/// Base object to query CoinMarketCap api.
#[derive(Default)]
pub struct CoinMarketCap {
api_key: String,
url: String,
client: Client,
}

impl CoinMarketCap {
/// initiate new api object.
pub fn new_with_api_key(api_key: String) -> Self {
Self {
api_key,
url: PRO_ENDPOINT.into(),
..Self::default()
}
}

pub async fn get_prices(&self, ids: &[&str]) -> Vec<Result<PriceInfo, Error>> {
match self._get_prices(ids).await {
Ok(results) => results,
Err(err) => {
tracing::trace!("get prices error: {}", err);
ids.iter()
.map(|_| Err(Error::GeneralQueryPriceError()))
.collect()
}
}
}

async fn _get_prices(&self, ids: &[&str]) -> Result<Vec<Result<PriceInfo, Error>>, Error> {
let response = self.send_request(ids).await?;

let parsed_response = response.json::<CoinMarketResponse>().await?;

let id_to_response = parsed_response
.data
.values()
.map(|coin_info| (coin_info.slug.clone(), coin_info.clone()))
.collect::<HashMap<String, CoinInfo>>();

let results = ids
.iter()
.map(|&id| {
let Some(price_info) = id_to_response.get(id) else {
return Err(Error::NotFound("price info".into()));
};

let naive_datetime = match NaiveDateTime::parse_from_str(
&price_info.last_updated,
"%Y-%m-%dT%H:%M:%S.%fZ",
) {
Ok(datetime) => datetime,
Err(err) => return Err(err.into()),
};

let datetime = Utc.from_utc_datetime(&naive_datetime);

Ok(PriceInfo {
id: id.to_string(),
price: price_info.quote.usd.price,
timestamp: datetime.timestamp() as u64,
})
})
.collect::<Vec<_>>();

Ok(results)
}

async fn send_request(&self, ids: &[&str]) -> Result<Response, Error> {
let query: Vec<(&str, String)> = vec![("slug", ids.join(","))];

let response = self
.client
.get(&self.url)
.header("X-CMC_PRO_API_KEY", &self.api_key)
.header("User-Agent", "BandPriceAdapter")
.query(&query)
.send()
.await?;
let response_status = response.status();
if response.status() != StatusCode::OK {
tracing::trace!(
"query request get error status {} {}",
response_status,
response.text().await?
);
return Err(Error::GeneralQueryPriceError());
}

Ok(response)
}
}
2 changes: 2 additions & 0 deletions price-adapter-raw/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@
mod binance_websocket;
mod coingecko;
mod coinmarketcap;
pub mod error;
pub mod types;

pub use binance_websocket::{BinanceWebsocket, BinanceWebsocketService};
pub use coingecko::CoinGecko;
pub use coinmarketcap::CoinMarketCap;
1 change: 1 addition & 0 deletions price-adapter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
## Unreleased

- Support CoinGecko sources
- Support CoinMarketCap source
- Support Binance Websocket source
- Create Interval and Websocket service to manage sources
21 changes: 21 additions & 0 deletions price-adapter/examples/coinmarketcap-interval-service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use price_adapter::services::IntervalService;
use price_adapter::sources::CoinMarketCap;
use price_adapter::types::{Service, Source};
use std::time::Duration;

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let coinmarketcap: CoinMarketCap<price_adapter::mappers::BandStaticMapper> =
CoinMarketCap::new_with_default("$API_KEY".into()).unwrap();
let mut service = IntervalService::new(coinmarketcap, Duration::from_secs(20));
service
.start(vec!["BAND", "AVAX"].as_slice())
.await
.unwrap();

loop {
tokio::time::sleep(Duration::from_secs(1)).await;
println!("{:?}", service.get_prices(&["BAND", "AVAX"]).await);
}
}
12 changes: 12 additions & 0 deletions price-adapter/examples/coinmarketcap-pro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use price_adapter::sources::CoinMarketCap;
use price_adapter::types::Source;

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let coingecko = CoinMarketCap::new_with_default("$API_KEY".into()).unwrap();
let queries = vec!["BAND", "AVAX"];
let prices: Vec<Result<price_adapter::types::PriceInfo, price_adapter::error::Error>> =
coingecko.get_prices(&queries).await;
println!("prices: {:?}", prices);
}
Loading

0 comments on commit 0e3a441

Please sign in to comment.