swap.rs•11.6 kB
//! Swap transaction building and simulation
use crate::abis::{UniswapV3SwapRouter, ERC20};
use crate::ethereum::{pool, utils};
use crate::types::{SwapParams, SwapQuote};
use alloy::primitives::{Address, U256};
use anyhow::{Context, Result};
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
use std::str::FromStr;
/// Get a swap quote without executing - simulates the transaction
pub async fn get_swap_quote<P: alloy::providers::Provider>(
params: &SwapParams,
pool_factory_address: Address,
provider: &P,
) -> Result<SwapQuote> {
tracing::info!("Getting quote for swap: {} -> {}", params.from_token, params.to_token);
// Parse addresses and amounts
let from_token: Address = params.from_token.parse()?;
let to_token: Address = params.to_token.parse()?;
let amount_in = Decimal::from_str(¶ms.amount_in).context("Invalid amount")?;
// Get the pool address and calculate estimated output based on pool price
let fee_tier = 500u64; // 0.05%
let pool_address = pool::get_uniswap_v3_pool_address(from_token, to_token, fee_tier, pool_factory_address, provider).await?;
let estimated_out = if pool_address != Address::ZERO {
// Calculate expected output using pool price
let pool_price = pool::get_pool_price(pool_address, provider).await?;
let (token0, token1) = pool::get_pool_tokens(pool_address, provider).await?;
// Determine output based on swap direction
let expected_out = if from_token == token0 && to_token == token1 {
amount_in * pool_price
} else if from_token == token1 && to_token == token0 {
amount_in / pool_price
} else {
// Fallback value if tokens don't match pool structure
Decimal::ZERO
};
expected_out.to_string()
} else {
// No pool found, return placeholder
"0".to_string()
};
Ok(SwapQuote {
from_token: params.from_token.clone(),
to_token: params.to_token.clone(),
amount_in: params.amount_in.clone(),
estimated_amount_out: estimated_out,
price_impact: "0.5%".to_string(),
gas_estimate: "350000".to_string(),
})
}
/// Check token balance for a swap
pub async fn check_token_balance_for_swap<P: alloy::providers::Provider>(
token_address: &str,
user_address: Address,
provider: &P,
) -> Result<String> {
tracing::info!("Checking balance for token: {:?}", token_address);
match get_token_balance_for_swap_impl(token_address, user_address, provider).await {
Ok(balance_str) => {
tracing::info!("Balance check result: {}", balance_str);
Ok(format!("✓ Balance: {}", balance_str))
}
Err(e) => {
tracing::warn!("Failed to check balance: {}", e);
Ok(format!("⚠ Could not check balance: {}", e))
}
}
}
async fn get_token_balance_for_swap_impl<P: alloy::providers::Provider>(
token_address: &str,
user_address: Address,
provider: &P,
) -> Result<String> {
let token_addr: Address = token_address.parse()
.context("Invalid token address")?;
let erc20 = ERC20::new(token_addr, provider);
let balance_result = erc20.balanceOf(user_address).call().await
.context("Failed to call balanceOf")?;
Ok(format!("{} tokens", balance_result))
}
/// Check token allowance for a swap
pub async fn check_token_allowance_for_swap<P: alloy::providers::Provider>(
token_address: Address,
user_address: Address,
router_address: Address,
required_amount: U256,
provider: &P,
) -> Result<String> {
tracing::info!("Checking allowance for router: {:?}", router_address);
let erc20 = ERC20::new(token_address, provider);
let allowance_result = erc20.allowance(user_address, router_address).call().await;
// Get token decimals
let decimals = get_token_decimals_helper(token_address, provider).await.unwrap_or(18u8);
match allowance_result {
Ok(allowance) => {
let allowance_val = allowance;
let denominator = utils::power_of_10(decimals as i32);
let allowance_decimal = Decimal::from(allowance_val.to::<u128>()) / denominator;
if allowance_val < required_amount {
Err(anyhow::anyhow!(
"Insufficient allowance. Required amount exceeds current allowance: {:.6}",
allowance_decimal
))
} else {
Ok(format!("✓ Allowance: {:.6} tokens", allowance_decimal))
}
}
Err(e) => {
tracing::warn!("Failed to check allowance: {}", e);
Ok(format!("⚠ Could not check allowance: {}", e))
}
}
}
async fn get_token_decimals_helper<P: alloy::providers::Provider>(
token_address: Address,
provider: &P,
) -> Result<u8> {
let erc20 = ERC20::new(token_address, provider);
let decimals_result = erc20.decimals().call().await
.context("Failed to call decimals()")?;
Ok(decimals_result)
}
/// Build and simulate a swap transaction using eth_call and eth_estimateGas
pub async fn build_swap_transaction<P: alloy::providers::Provider>(
params: &SwapParams,
router_address: Address,
pool_factory_address: Address,
user_address: Address,
provider: &P,
) -> Result<String> {
tracing::info!("Building swap transaction for {} -> {}", params.from_token, params.to_token);
// Parse addresses and amounts
let from_token: Address = params.from_token.parse()?;
let to_token: Address = params.to_token.parse()?;
let amount_in = Decimal::from_str(¶ms.amount_in).context("Invalid amount")?;
tracing::info!(
"Parsed inputs -> from_token: {:?}, to_token: {:?}, amount_in: {}",
from_token,
to_token,
amount_in
);
// Check that a Uniswap V3 pool exists for this pair at the selected fee tier
let fee_tier = 500u64; // 0.05%
let pool_address = pool::get_uniswap_v3_pool_address(from_token, to_token, fee_tier, pool_factory_address, provider).await?;
tracing::info!("Resolved pool address: {:?}", pool_address);
if pool_address == Address::ZERO {
return Err(anyhow::anyhow!(
"No Uniswap V3 pool found for the token pair {}-{} at fee tier {} (0.05%)",
from_token,
to_token,
fee_tier
));
}
// Get token decimals
let from_decimals = get_token_decimals_helper(from_token, provider).await.unwrap_or(18u8);
let denominator = utils::power_of_10(from_decimals as i32);
let scaled_amount = U256::from((amount_in * denominator).to_u64().context("Amount too large for scaling")?);
tracing::info!(
"From token decimals: {}, scaled amount_in (wei units): {}",
from_decimals,
scaled_amount
);
let quote = get_swap_quote(params, pool_factory_address, provider).await?;
tracing::info!("Quote estimated_amount_out: {} {}", quote.estimated_amount_out, params.to_token);
// Get token decimals for output scaling
let to_decimals = get_token_decimals_helper(to_token, provider).await.unwrap_or(18u8);
// Calculate minimum amount out based on price, amount_in, and slippage
let slippage = params.slippage_tolerance.unwrap_or(Decimal::from_str("0.5")?);
tracing::info!("Using slippage_tolerance: {}%", slippage);
// Use the dedicated function to calculate min amount out
let min_amount_out = pool::calculate_min_amount_out(
pool_address,
from_token,
to_token,
amount_in,
slippage,
provider,
).await
.map_err(|e| {
tracing::warn!("Failed to calculate min amount out using pool price: {}, falling back to quote estimate", e);
anyhow::anyhow!("Calculation error: {}", e)
})
.or_else(|_| -> Result<Decimal> {
// Fallback to using quote estimate if pool price calculation fails
tracing::info!("Falling back to quote-based min amount out calculation");
let amount_out = Decimal::from_str("e.estimated_amount_out).unwrap_or(Decimal::ZERO);
let min = amount_out * (Decimal::from(1) - slippage / Decimal::from(100));
Ok(min)
})?;
tracing::info!("Calculated min_amount_out: {} {}", min_amount_out, params.to_token);
let to_denominator = utils::power_of_10(to_decimals as i32);
let scaled_min_out = U256::from((min_amount_out * to_denominator).to_u64().context("Min amount out too large")?);
tracing::debug!("Scaled min_amount_out (wei units): {}", scaled_min_out);
// Create Uniswap V3 Router instance
let router = UniswapV3SwapRouter::new(router_address, provider);
// Recipient and deadline for swap
let recipient = params.recipient.clone()
.unwrap_or_else(|| format!("{:?}", user_address))
.parse::<Address>()?;
tracing::info!("Recipient: {:?}", recipient);
// Calculate swap path using the dedicated function
let path_bytes = utils::calculate_path(from_token, to_token, fee_tier);
// Build the input parameters struct
let exact_input_params = UniswapV3SwapRouter::ExactInputParams {
path: path_bytes,
recipient,
amountIn: scaled_amount,
amountOutMinimum: scaled_min_out,
};
// Use eth_call to simulate the swap (no actual execution)
let simulate_result = router.exactInput(exact_input_params.clone()).call().await;
tracing::info!("simulate_result {:?}", simulate_result);
// Also estimate gas for the actual transaction
let estimate_result = router.exactInput(exact_input_params).estimate_gas().await;
tracing::info!("estimate_result {:?}", estimate_result);
if let Err(ref e) = estimate_result {
tracing::warn!("Gas estimation error: {}", e);
}
// Format results
let gas_estimate_msg = match estimate_result {
Ok(gas) => format!("Estimated Gas: {} wei", gas),
Err(e) => format!("Gas estimation failed: {}", e)
};
let simulation_msg = match simulate_result {
Ok(n) => format!("✓ Simulation successful({}) - transaction would succeed", n),
Err(e) => format!("⚠ Simulation warning: {}", e)
};
Ok(format!(
"Swap Transaction Simulation (using eth_call):\nSwap Details:\n From: {} {}\n To: ~{} {}\n Min Out ({}% slippage): ~{} {}\n {}\n {}\n\nNote: This used eth_call for simulation. No transaction has been submitted to the blockchain.",
params.amount_in, params.from_token,
quote.estimated_amount_out, params.to_token,
slippage, min_amount_out, params.to_token,
gas_estimate_msg,
simulation_msg
))
}
/// Validate swap parameters
pub fn validate_swap_params(params: &SwapParams) -> Result<()> {
// Validate addresses are valid Ethereum addresses
if !params.from_token.starts_with("0x") || params.from_token.len() != 42 {
return Err(anyhow::anyhow!("Invalid from_token address"));
}
if !params.to_token.starts_with("0x") || params.to_token.len() != 42 {
return Err(anyhow::anyhow!("Invalid to_token address"));
}
// Validate amount
Decimal::from_str(¶ms.amount_in)
.context("Invalid amount_in")?;
// Validate slippage
if let Some(slippage) = params.slippage_tolerance {
if slippage < Decimal::ZERO || slippage > Decimal::from(100) {
return Err(anyhow::anyhow!("Slippage must be between 0 and 100"));
}
}
Ok(())
}