eth-prices 0.0.11

A library for fetching Ethereum prices.
Documentation
use alloy::{
    primitives::BlockNumber,
    providers::{Provider, ProviderBuilder},
};
use wasm_bindgen::prelude::*;

use super::{
    convert::{into_js_error, parse_address, parse_token_identifier, parse_u256},
    route::Route,
    types::{
        AutoRouterConfig, ChainlinkQuoterConfig, CreateEngineConfig, JsCreateEngineConfig,
        QuoteRequest,
    },
};
use crate::{
    Result,
    network::{NetworkId, NetworkInstant},
    provider::RpcProvider,
    quoter::{
        AnyQuoter,
        chainlink::ChainlinkQuoter,
        erc4626::ERC4626Quoter,
        fixed::FixedQuoter,
        uniswap_v2::{UniswapV2Quoter, UniswapV2Selector},
        uniswap_v3::{UniswapV3Quoter, discovery::UniswapV3Selector},
    },
    router::{AutoRouter, Router},
};

#[cfg(feature = "ecb")]
use crate::quoter::ecb::EcbRateSource;

#[wasm_bindgen]
pub struct Engine {
    provider: RpcProvider,
    network_id: NetworkId,
    router: Router,
}

#[wasm_bindgen]
impl Engine {
    #[wasm_bindgen(js_name = addFixedQuoter)]
    pub fn add_fixed_quoter(&mut self, quoter: FixedQuoter) -> Result<(), JsError> {
        self.push_quoter(quoter.into());
        Ok(())
    }

    #[wasm_bindgen(js_name = addUniswapV2Quoter)]
    pub async fn add_uniswap_v2_quoter(
        &mut self,
        selector: UniswapV2Selector,
    ) -> Result<(), JsError> {
        let quoter = UniswapV2Quoter::from_selector(&self.provider, selector)
            .await
            .map_err(into_js_error)?;
        self.push_quoter(quoter.into());
        Ok(())
    }

    #[wasm_bindgen(js_name = addUniswapV3Quoter)]
    pub async fn add_uniswap_v3_quoter(
        &mut self,
        selector: UniswapV3Selector,
    ) -> Result<(), JsError> {
        let quoter = UniswapV3Quoter::from_selector(&self.provider, selector)
            .await
            .map_err(into_js_error)?;
        self.push_quoter(quoter.into());
        Ok(())
    }

    #[wasm_bindgen(js_name = addErc4626Quoter)]
    pub async fn add_erc4626_quoter(&mut self, vault_address: String) -> Result<(), JsError> {
        let vault_address = parse_address(&vault_address)?;
        let quoter = ERC4626Quoter::new(vault_address, &self.provider)
            .await
            .map_err(into_js_error)?;
        self.push_quoter(quoter.into());
        Ok(())
    }

    #[wasm_bindgen(js_name = addChainlinkQuoter)]
    pub async fn add_chainlink_quoter(
        &mut self,
        config: ChainlinkQuoterConfig,
    ) -> Result<(), JsError> {
        let quoter = self.create_chainlink_quoter(config).await?;
        self.push_quoter(quoter.into());
        Ok(())
    }

    #[cfg(feature = "ecb")]
    #[wasm_bindgen(js_name = addEcbQuoters)]
    pub fn add_ecb_quoters(&mut self) -> Result<(), JsError> {
        self.load_ecb();
        Ok(())
    }

    #[wasm_bindgen(js_name = addAutoRouter)]
    pub async fn add_auto_router(&mut self, config: AutoRouterConfig) -> Result<(), JsError> {
        let router = self.create_auto_router(config).await?;
        self.router.merge_with(router);
        Ok(())
    }

    #[wasm_bindgen(js_name = computeRoute)]
    pub fn compute_route(
        &self,
        input_token: String,
        output_token: String,
    ) -> Result<Route, JsError> {
        let input_token = parse_token_identifier(&input_token)?;
        let output_token = parse_token_identifier(&output_token)?;
        self.router
            .compute(&input_token, &output_token)
            .map(Route::from)
            .map_err(into_js_error)
    }

    #[wasm_bindgen(js_name = quoteRoute)]
    pub async fn quote_route(
        &self,
        route: &Route,
        amount_in: String,
        block: Option<u64>,
        fiat_timestamp: Option<u64>,
    ) -> Result<String, JsError> {
        let amount_in = parse_u256(&amount_in)?;
        let networks = self.resolve_networks(block, fiat_timestamp).await?;
        route
            .inner
            .quote(&networks, amount_in)
            .await
            .map(|amount_out| amount_out.to_string())
            .map_err(into_js_error)
    }

    #[wasm_bindgen(js_name = getRate)]
    pub async fn get_rate(
        &self,
        input_token: String,
        output_token: String,
        amount_in: String,
        block: Option<u64>,
        fiat_timestamp: Option<u64>,
    ) -> Result<String, JsError> {
        let route = self.compute_route(input_token, output_token)?;
        self.quote_route(&route, amount_in, block, fiat_timestamp)
            .await
    }

    #[wasm_bindgen(js_name = quote)]
    pub async fn quote(&self, request: QuoteRequest) -> Result<String, JsError> {
        self.get_rate(
            request.input_token,
            request.output_token,
            request.amount_in,
            request.block,
            request.fiat_timestamp,
        )
        .await
    }

    #[wasm_bindgen(js_name = getLatestBlock)]
    pub async fn get_latest_block(&self) -> Result<u64, JsError> {
        self.provider
            .get_block_number()
            .await
            .map_err(into_js_error)
    }

    #[wasm_bindgen(js_name = listQuoters)]
    pub fn list_quoters(&self) -> Vec<String> {
        self.router
            .quoters
            .iter()
            .map(|quoter| quoter.to_string())
            .collect()
    }
}

impl Engine {
    fn push_quoter(&mut self, quoter: AnyQuoter) {
        self.router.add_quoter(quoter);
    }

    async fn resolve_block(&self, block: Option<u64>) -> Result<BlockNumber, JsError> {
        match block {
            Some(block) => Ok(block),
            None => self
                .provider
                .get_block_number()
                .await
                .map_err(into_js_error),
        }
    }

    async fn resolve_networks(
        &self,
        block: Option<u64>,
        fiat_timestamp: Option<u64>,
    ) -> Result<NetworkInstant, JsError> {
        let block = self.resolve_block(block).await?;
        let mut networks = NetworkInstant::default().with_evm_block(
            self.network_id.clone(),
            block,
            self.provider.clone(),
        );

        if let Some(fiat_timestamp) = fiat_timestamp {
            networks = networks.with_fiat_timestamp(fiat_timestamp);
        }

        Ok(networks)
    }

    async fn from_config(config: CreateEngineConfig) -> Result<Self, JsError> {
        let rpc_url = config.rpc_url.unwrap_or_default();
        if rpc_url.trim().is_empty() {
            return Err(JsError::new("rpcUrl is required"));
        }
        let provider = ProviderBuilder::new()
            .connect(&rpc_url)
            .await
            .map_err(into_js_error)?
            .erased();
        let network_id = NetworkId::from_provider(&provider)
            .await
            .map_err(into_js_error)?;

        let mut quoter = Self {
            provider,
            network_id,
            router: Router::default(),
        };

        quoter.load_fixed(config.quoters.fixed);
        quoter.load_uniswap_v2(config.quoters.uniswap_v2).await?;
        quoter.load_uniswap_v3(config.quoters.uniswap_v3).await?;
        quoter.load_erc4626(config.quoters.erc4626).await?;
        quoter.load_chainlink(config.quoters.chainlink).await?;
        #[cfg(feature = "ecb")]
        if config.quoters.ecb {
            quoter.load_ecb();
        }
        if let Some(config) = config.quoters.auto {
            let router = quoter.create_auto_router(config).await?;
            quoter.router.merge_with(router);
        }

        Ok(quoter)
    }

    fn load_fixed(&mut self, quoters: Vec<FixedQuoter>) {
        for quoter in quoters {
            self.push_quoter(quoter.into());
        }
    }

    async fn load_uniswap_v2(&mut self, selectors: Vec<UniswapV2Selector>) -> Result<(), JsError> {
        for selector in selectors {
            let quoter = UniswapV2Quoter::from_selector(&self.provider, selector)
                .await
                .map_err(into_js_error)?;
            self.push_quoter(quoter.into());
        }
        Ok(())
    }

    async fn load_uniswap_v3(&mut self, selectors: Vec<UniswapV3Selector>) -> Result<(), JsError> {
        for selector in selectors {
            let quoter = UniswapV3Quoter::from_selector(&self.provider, selector)
                .await
                .map_err(into_js_error)?;
            self.push_quoter(quoter.into());
        }
        Ok(())
    }

    async fn load_erc4626(
        &mut self,
        vault_addresses: Vec<alloy::primitives::Address>,
    ) -> Result<(), JsError> {
        for vault_address in vault_addresses {
            let quoter = ERC4626Quoter::new(vault_address, &self.provider)
                .await
                .map_err(into_js_error)?;
            self.push_quoter(quoter.into());
        }
        Ok(())
    }

    async fn load_chainlink(&mut self, configs: Vec<ChainlinkQuoterConfig>) -> Result<(), JsError> {
        for config in configs {
            let quoter = self.create_chainlink_quoter(config).await?;
            self.push_quoter(quoter.into());
        }
        Ok(())
    }

    #[cfg(feature = "ecb")]
    fn load_ecb(&mut self) {
        self.router.merge_with(EcbRateSource::default().graph());
    }

    async fn create_chainlink_quoter(
        &self,
        config: ChainlinkQuoterConfig,
    ) -> Result<ChainlinkQuoter, JsError> {
        ChainlinkQuoter::new(
            config.contract,
            config.token,
            config.token_decimals,
            config.quote,
            config.quote_decimals,
            &self.provider,
        )
        .await
        .map_err(into_js_error)
    }

    async fn create_auto_router(&self, config: AutoRouterConfig) -> Result<Router, JsError> {
        let mut auto = AutoRouter::new(self.provider.clone(), config.tokens);
        if let Some(network_id) = config.network_id {
            auto = auto.with_network_id(network_id.into());
        }
        if let Some(factory) = config.uniswap_v2_factory {
            auto = auto.with_uniswap_v2_factory(factory);
        }
        if let Some(factory) = config.uniswap_v3_factory {
            auto = auto.with_uniswap_v3_factory(factory);
        }
        if let Some(fees) = config.uniswap_v3_fees {
            auto = auto.with_uniswap_v3_fees(fees);
        }
        if let Some(min_liquidity) = config.min_liquidity {
            auto = auto.with_min_liquidity(min_liquidity);
        }
        if let Some(enable) = config.discover_uniswap_v2 {
            auto = auto.discover_uniswap_v2(enable);
        }
        if let Some(enable) = config.discover_uniswap_v3 {
            auto = auto.discover_uniswap_v3(enable);
        }
        if let Some(enable) = config.discover_erc4626 {
            auto = auto.discover_erc4626(enable);
        }
        auto.build().await.map_err(into_js_error)
    }
}

#[wasm_bindgen(js_name = createEngine)]
pub async fn create_engine(config: JsCreateEngineConfig) -> Result<Engine, JsError> {
    let config: CreateEngineConfig =
        serde_wasm_bindgen::from_value(config.into()).map_err(|e| JsError::new(&e.to_string()))?;
    Engine::from_config(config).await
}