use riglr_core::{signer::SignerContext, ToolError};
use riglr_macros::tool;
use serde::{Deserialize, Serialize};
use tracing::debug;
alloy::sol! {
interface IQuoterV2 {
function quoteExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256 amountIn,
uint160 sqrtPriceLimitX96
) external returns (
uint256 amountOut,
uint160 sqrtPriceX96After,
uint32 initializedTicksCrossed,
uint256 gasEstimate
);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwapQuote {
pub amount_out: String,
pub amount_in: String,
pub token_in: String,
pub token_out: String,
pub price: String,
pub price_impact: String,
pub gas_estimate: String,
pub route: Vec<String>,
pub fee_tier: String,
pub amount_out_minimum: String,
}
async fn get_uniswap_quote_typed(
token_in_addr: alloy::primitives::Address,
token_out_addr: alloy::primitives::Address,
amount_in_wei: alloy::primitives::U256,
fee: u32,
quoter_address: &str,
client: &dyn alloy::providers::Provider,
) -> Result<(alloy::primitives::U256, alloy::primitives::U256), ToolError> {
use alloy::primitives::{Address, Bytes};
use std::str::FromStr;
let quoter_addr = Address::from_str(quoter_address)
.map_err(|e| ToolError::permanent_string(format!("Invalid quoter address: {}", e)))?;
use alloy::primitives::aliases::U24;
use alloy::sol_types::SolCall;
let call = IQuoterV2::quoteExactInputSingleCall {
tokenIn: token_in_addr,
tokenOut: token_out_addr,
fee: U24::from(fee),
amountIn: amount_in_wei,
sqrtPriceLimitX96: alloy::primitives::U160::ZERO,
};
let encoded = call.abi_encode();
let tx_request = alloy::rpc::types::TransactionRequest::default()
.to(quoter_addr)
.input(Bytes::from(encoded).into());
let result_bytes = client
.call(tx_request)
.await
.map_err(|e| ToolError::retriable_string(format!("Failed to get Uniswap quote: {}", e)))?;
let decoded =
<IQuoterV2::quoteExactInputSingleCall as SolCall>::abi_decode_returns(&result_bytes)
.map_err(|e| {
ToolError::permanent_string(format!("Failed to decode quote result: {}", e))
})?;
Ok((decoded.amountOut, decoded.gasEstimate))
}
#[tool]
#[allow(clippy::too_many_arguments)]
pub async fn get_uniswap_quote(
token_in: String,
token_out: String,
amount_in: String,
decimals_in: u8,
decimals_out: u8,
fee_tier: Option<u32>,
slippage: Option<u32>,
chain_id: Option<u64>,
context: &riglr_core::provider::ApplicationContext,
) -> Result<SwapQuote, ToolError> {
use alloy::primitives::{Address, U256};
use std::str::FromStr;
let token_in_addr = Address::from_str(&token_in)
.map_err(|e| ToolError::permanent_string(format!("Invalid token_in address: {}", e)))?;
let token_out_addr = Address::from_str(&token_out)
.map_err(|e| ToolError::permanent_string(format!("Invalid token_out address: {}", e)))?;
let amount_in_wei = U256::from_str(&amount_in)
.map_err(|e| ToolError::permanent_string(format!("Invalid amount_in: {}", e)))?;
let provider = context
.get_extension::<std::sync::Arc<dyn alloy::providers::Provider>>()
.ok_or_else(|| ToolError::permanent_string("Provider not found in context".to_string()))?;
let client = &**provider;
let resolved_chain_id = if let Some(id) = chain_id {
debug!("Using explicit chain_id: {}", id);
id
} else if let Ok(signer) = SignerContext::current_as_evm().await {
let id = signer.chain_id();
debug!("Using chain_id from EVM SignerContext: {}", id);
id
} else {
debug!("No explicit chain_id or EVM signer context, defaulting to Ethereum mainnet (1)");
1 };
let quoter_address = context
.config
.network
.get_chain(resolved_chain_id)
.and_then(|chain| chain.contracts.quoter.as_ref())
.ok_or_else(|| {
ToolError::permanent_string(format!(
"Uniswap V3 quoter address not configured for chain_id {}. Please add it to chains.toml or set QUOTER_{} environment variable",
resolved_chain_id, resolved_chain_id
))
})?;
let fee = fee_tier.unwrap_or(3000);
let (amount_out_wei, gas_estimate_wei) = get_uniswap_quote_typed(
token_in_addr,
token_out_addr,
amount_in_wei,
fee,
quoter_address,
client,
)
.await?;
let divisor_out = U256::from(10u64).pow(U256::from(decimals_out));
let amount_out_whole = amount_out_wei / divisor_out;
let amount_out_fraction = amount_out_wei % divisor_out;
let amount_out_formatted = if amount_out_fraction.is_zero() {
amount_out_whole.to_string()
} else {
let fraction_str = format!(
"{:0>width$}",
amount_out_fraction,
width = decimals_out as usize
);
let trimmed = fraction_str.trim_end_matches('0');
if trimmed.is_empty() {
amount_out_whole.to_string()
} else {
format!("{}.{}", amount_out_whole, trimmed)
}
};
let divisor_in = U256::from(10u64).pow(U256::from(decimals_in));
let price = if !amount_in_wei.is_zero() {
let price_raw = amount_out_wei * divisor_in / amount_in_wei / divisor_out;
format!(
"{}.{}",
price_raw / U256::from(1000),
price_raw % U256::from(1000)
)
} else {
"0.0".to_string()
};
let slippage_bps = slippage.unwrap_or(50); let slippage_factor = U256::from(10000 - slippage_bps);
let amount_out_minimum_wei = amount_out_wei * slippage_factor / U256::from(10000);
let amount_out_min_whole = amount_out_minimum_wei / divisor_out;
let amount_out_min_fraction = amount_out_minimum_wei % divisor_out;
let amount_out_minimum = if amount_out_min_fraction.is_zero() {
amount_out_min_whole.to_string()
} else {
let fraction_str = format!(
"{:0>width$}",
amount_out_min_fraction,
width = decimals_out as usize
);
let trimmed = fraction_str.trim_end_matches('0');
if trimmed.is_empty() {
amount_out_min_whole.to_string()
} else {
format!("{}.{}", amount_out_min_whole, trimmed)
}
};
let price_impact = format!("{}%", fee as f64 / 10000.0);
let fee_tier_str = format!("{}%", fee as f64 / 10000.0);
Ok(SwapQuote {
amount_out: amount_out_formatted,
amount_in,
token_in,
token_out,
price,
price_impact,
gas_estimate: gas_estimate_wei.to_string(),
route: vec![format!("Uniswap V3 {} pool", fee_tier_str)],
fee_tier: fee_tier_str,
amount_out_minimum,
})
}