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
}