data3 0.1.0

SDK for accessing ethereum data hosted by indexers.
Documentation
use std::collections::HashMap;

use alloy::primitives::Address;
use types::{ApiResponse, Candle, Granularity, Protocols};

use crate::{network::Network, serde_override::SerdeResponseParse};

pub mod types;

pub struct OneInch {
    api_key: String,
}

impl OneInch {
    pub fn new(api_key: String) -> Self {
        OneInch { api_key }
    }

    pub async fn prices(&self, network: Network) -> crate::Result<HashMap<Address, String>> {
        if !matches!(
            network,
            Network::EthereumMainnet
                | Network::OptimismMainnet
                | Network::ArbitrumMainnet
                | Network::AvalancheMainnet
                | Network::BscMainnet
                | Network::FantomOperaMainnet
                | Network::GnosisMainnet
                | Network::PolygonMainnet
                | Network::ZkSyncMainnet
                | Network::BaseMainnet
                | Network::LineaMainnet
        ) {
            return Err(crate::Error::ChainIdNotSupported(
                "1inch::prices",
                network.chain_id(),
            ));
        }

        let url = format!("https://api.1inch.dev/price/v1.1/{}", network.chain_id());

        let client = reqwest::Client::new();
        let response = client
            .get(url)
            .bearer_auth(self.api_key.clone())
            .query(&[("currency", "USD")])
            .send()
            .await?;

        response.serde_parse_custom().await
    }

    pub async fn balances(
        &self,
        address: Address,
        network: Network,
    ) -> crate::Result<HashMap<Address, String>> {
        let url = format!(
            "https://api.1inch.dev/balance/v1.2/{}/balances/{}",
            network.chain_id(),
            address
        );
        let client = reqwest::Client::new();
        let result = client
            .get(url)
            .bearer_auth(self.api_key.clone())
            .send()
            .await?
            .serde_parse_custom::<HashMap<Address, String>>()
            .await?;

        Ok(result
            .into_iter()
            .filter(|(_, balance)| *balance != "0")
            .collect::<HashMap<Address, String>>())
    }

    pub async fn portfolio(
        &self,
        network: Network,
        addresses: Vec<Address>,
        use_cache: bool,
    ) -> crate::Result<ApiResponse<Protocols>> {
        let url = "https://api.1inch.dev/portfolio/portfolio/v4/overview/protocols/current_value";
        let client = reqwest::Client::new();
        let response = client
            .get(url)
            .bearer_auth(self.api_key.clone())
            .query(&[(
                "addresses",
                addresses
                    .iter()
                    .map(|a| a.to_string())
                    .collect::<Vec<_>>()
                    .join(","),
            )])
            .query(&[("chain_id", network.chain_id())])
            .query(&[("use_cache", use_cache)])
            .send()
            .await?;

        response.serde_parse_custom().await
    }

    pub async fn ohlc(
        &self,
        token0: Address,
        token1: Address,
        network: Network,
        granularity: Granularity,
        order_asc: Option<bool>,
        limit: Option<usize>,
    ) -> crate::Result<Vec<Candle>> {
        // let url = "https://api.1inch.dev/portfolio/integrations/prices/v2/time_range/cross_prices";
        let url = "https://api.1inch.dev/portfolio/integrations/prices/v1/time_range/cross_prices";
        let client = reqwest::Client::new();
        let mut query_params = vec![
            ("token0_address", token0.to_string()),
            ("token1_address", token1.to_string()),
            ("chain_id", network.chain_id().to_string()),
            // ("from_timestamp", from_timestamp.to_string()),
            // ("to_timestamp", to_timestamp.to_string()),
            ("granularity", granularity.into()),
        ];
        if let Some(order) = order_asc {
            query_params.push(("order_asc", order.to_string()));
        }
        if let Some(lim) = limit {
            query_params.push(("limit", lim.to_string()));
        }

        client
            .get(url)
            .bearer_auth(self.api_key.clone())
            .query(&query_params)
            .send()
            .await?
            .serde_parse_custom()
            .await
    }
}

#[cfg(test)]
mod tests {
    use alloy::primitives::address;
    use rstest::rstest;

    use super::*;
    use crate::network::Network;

    #[rstest]
    #[case(Network::EthereumMainnet)]
    #[case(Network::OptimismMainnet)]
    #[case(Network::ArbitrumMainnet)]
    #[tokio::test]
    async fn test_prices(#[case] network: Network) {
        let api_key = std::env::var("ONEINCH_API_KEY").expect("ONEINCH_API_KEY key not set in env");
        let oneinch = OneInch::new(api_key);
        let result = oneinch.prices(network).await.unwrap();

        println!("1inch prices: {:?}", result);
        // assert!(false);
    }

    #[rstest]
    #[case(Network::EthereumMainnet)]
    #[case(Network::OptimismMainnet)]
    #[case(Network::ArbitrumMainnet)]
    #[tokio::test]
    async fn test_balances(#[case] network: Network) {
        let api_key = std::env::var("ONEINCH_API_KEY").expect("ONEINCH_API_KEY key not set in env");
        let oneinch = OneInch::new(api_key);

        let balances = oneinch
            .balances(
                address!("0xf51C53b2C184Aa43A7d24aA4f0Cf13D0e8b56c51"),
                network,
            )
            .await
            .unwrap();

        println!("1inch balances: {:?}", balances);
        // assert!(false);
    }

    #[rstest]
    #[case(Network::EthereumMainnet)]
    #[case(Network::OptimismMainnet)]
    #[case(Network::ArbitrumMainnet)]
    #[tokio::test]
    async fn test_portfolio(#[case] network: Network) {
        let api_key = std::env::var("ONEINCH_API_KEY").expect("ONEINCH_API_KEY key not set in env");
        let oneinch = OneInch::new(api_key);
        let addresses = vec![address!("0xf51C53b2C184Aa43A7d24aA4f0Cf13D0e8b56c51")];
        let result = oneinch.portfolio(network, addresses, false).await.unwrap();
        println!("1inch portfolio: {:?}", result);
        // assert!(false);
    }

    #[tokio::test]
    async fn test_ohlc() {
        let api_key = std::env::var("ONEINCH_API_KEY").expect("ONEINCH_API_KEY key not set in env");
        let oneinch = OneInch::new(api_key);
        let result = oneinch
            .ohlc(
                address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
                address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
                Network::EthereumMainnet,
                Granularity::Hour,
                Some(true),
                Some(100),
            )
            .await
            .unwrap();

        println!("1inch OHLC: {:?}", result);

        // assert!(false);
    }
}