waterpump-evm-pool-sdk 0.1.0

EVM pool SDK — viewers, infusers, harvesters, swappers for Uniswap V3/V4, PancakeSwap, Slipstream, Shadow, Algebra
Documentation
use alloy::{
    primitives::{aliases::U128, Address, Bytes, TxHash, U256},
    rpc::types::Log,
    sol,
};
use alloy_sol_types::SolCall;
use anyhow::Result;
use tracing::{debug, error, info, instrument};
use uniswap_sdk_core::prelude::{Currency, CurrencyAmount, ToBig, TradeType};
use uniswap_v3_sdk::prelude::{IQuoter, IQuoterV2};

use crate::{
    pool_swappers::common::SwapEventData,
    types::swap_results::{V3SwapResult, V3SwapWithIntermediateResult},
};

sol! {
    /// @notice Emitted by the pool for any swaps between token0 and token1
    /// @param sender The address that initiated the swap call, and that received the callback
    /// @param recipient The address that received the output of the swap
    /// @param amount0 The delta of the token0 balance of the pool
    /// @param amount1 The delta of the token1 balance of the pool
    /// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96
    /// @param liquidity The liquidity of the pool after the swap
    /// @param tick The log base 1.0001 of price of the pool after the swap
    event Swap(
        address indexed sender,
        address indexed recipient,
        int256 amount0,
        int256 amount1,
        uint160 sqrtPriceX96,
        uint128 liquidity,
        int24 tick
    );
}

#[instrument(skip(res), fields(response_len = res.len(), trade_type = ?trade_type, use_quoter_v2 = use_quoter_v2))]
pub fn decode_quote_amount(
    res: Bytes,
    trade_type: &TradeType,
    use_quoter_v2: bool,
) -> Result<U256> {
    let amount = match trade_type {
        TradeType::ExactInput => {
            if use_quoter_v2 {
                IQuoterV2::quoteExactInputSingleCall::abi_decode_returns_validate(res.as_ref())
                    .unwrap()
                    .amountOut
            } else {
                IQuoter::quoteExactInputSingleCall::abi_decode_returns_validate(res.as_ref())
                    .unwrap()
            }
        }
        TradeType::ExactOutput => {
            if use_quoter_v2 {
                IQuoterV2::quoteExactOutputSingleCall::abi_decode_returns_validate(res.as_ref())
                    .unwrap()
                    .amountIn
            } else {
                IQuoter::quoteExactOutputSingleCall::abi_decode_returns_validate(res.as_ref())
                    .unwrap()
            }
        }
    };

    Ok(amount)
}

/// Decode Swap event from a log entry
///
/// # Arguments
/// * `log` - The log entry from a transaction receipt
/// * `pool_address` - The address of the Uniswap V3 pool (optional, for
///   validation)
///
/// # Returns
/// Returns the decoded Swap event log, or an error if the log doesn't match the
/// Swap event signature
///
/// # Example
/// ```no_run
/// use waterpump_evm_pool_sdk::pool_swappers::v3::decoder::decode_swap_event;
/// use alloy::rpc::types::Log;
///
/// // Assuming you have a log from a transaction receipt
/// let swap_log = decode_swap_event(&log, Some(pool_address))?;
/// // Access event data: swap_log.inner.sender, swap_log.inner.amount0, etc.
/// ```
#[instrument(skip(log), fields(log_address = ?log.address(), pool_address = ?pool_address))]
pub fn decode_swap_event(log: &Log, pool_address: Option<Address>) -> Result<Log<Swap>> {
    // Validate that this log is from the expected pool address if provided
    if let Some(expected_address) = pool_address {
        if log.address() != expected_address {
            error!(
                log_address = ?log.address(),
                expected_address = ?expected_address,
                "Log address does not match expected pool address"
            );
            return Err(anyhow::anyhow!(
                "Log address {:?} does not match expected pool address {:?}",
                log.address(),
                expected_address
            ));
        }
    }

    // Decode the Swap event from the log
    let result = log.log_decode::<Swap>().map_err(|e| {
        debug!(error = ?e, "Failed to decode Swap event");
        anyhow::anyhow!("Failed to decode Swap event: {:?}", e)
    });
    if result.is_ok() {
        debug!("Successfully decoded swap event");
    }
    result
}

/// Decode all Swap events from transaction receipt logs
///
/// # Arguments
/// * `logs` - The logs from a transaction receipt
/// * `pool_address` - The address of the Uniswap V3 pool (optional, for
///   filtering)
///
/// # Returns
/// Returns a vector of decoded Swap event logs
///
/// # Example
/// ```no_run
/// use waterpump_evm_pool_sdk::pool_swappers::v3::decoder::decode_swap_events_from_logs;
///
/// // Assuming you have logs from a transaction receipt
/// let swap_events = decode_swap_events_from_logs(&receipt.logs, Some(pool_address))?;
/// for swap_log in swap_events {
///     println!("Swap: amount0={:?}, amount1={:?}",
///              swap_log.inner.amount0,
///              swap_log.inner.amount1);
/// }
/// ```
#[instrument(skip(logs), fields(log_count = logs.len(), pool_address = ?pool_address))]
pub fn decode_swap_events_from_logs(
    logs: &[Log],
    pool_address: Option<Address>,
) -> Result<Vec<Log<Swap>>> {
    let mut swap_events = Vec::new();

    for log in logs {
        // Try to decode as Swap event
        if let Ok(swap_event) = decode_swap_event(log, pool_address) {
            swap_events.push(swap_event);
        }
    }

    info!(
        total_logs = logs.len(),
        decoded_swap_events = swap_events.len(),
        "Finished decoding swap events"
    );
    Ok(swap_events)
}

/// Decode swap event from log and convert to V3SwapResult
///
/// This function:
/// 1. Decodes a log as a Swap event
/// 2. Extracts swap event data
/// 3. Calculates amount_in and amount_out based on swap direction
/// 4. Returns a V3SwapResult
///
/// # Arguments
///
/// * `log` - The log entry from a transaction receipt
/// * `tx_hash` - The transaction hash
/// * `is_to_b` - If true, swap is from token A to token B; if false, from token
///   B to token A
///
/// # Returns
///
/// Returns a `V3SwapResult` if the log is a valid Swap event, or an error
/// otherwise.
#[instrument(skip(log), fields(
    log_address = ?log.address(),
    tx_hash = ?tx_hash,
    is_to_b = is_to_b
))]
pub fn decode_swap_event_to_result(
    log: &Log,
    tx_hash: TxHash,
    is_to_b: bool,
    currency0: Currency,
    currency1: Currency,
) -> Result<V3SwapResult> {
    let decoded = log.log_decode::<Swap>()?;
    let swap_data = SwapEventData {
        sender: decoded.inner.sender,
        recipient: decoded.inner.recipient,
        amount0: decoded.inner.amount0,
        amount1: decoded.inner.amount1,
        sqrt_price_x96: decoded.inner.sqrtPriceX96,
        liquidity: decoded.inner.liquidity,
        tick: decoded.inner.tick,
    };

    let (amount_in, amount_out, currency_in, currency_out) = if is_to_b {
        (swap_data.amount0.unsigned_abs(), swap_data.amount1.unsigned_abs(), currency0, currency1)
    } else {
        (swap_data.amount1.unsigned_abs(), swap_data.amount0.unsigned_abs(), currency1, currency0)
    };

    Ok(V3SwapResult {
        tx_hash,
        sender: swap_data.sender,
        recipient: swap_data.recipient,
        amount_in: CurrencyAmount::from_raw_amount(currency_in, amount_in.to_big_int())?,
        amount_out: CurrencyAmount::from_raw_amount(currency_out, amount_out.to_big_int())?,
        sqrt_price_x96: swap_data.sqrt_price_x96,
        liquidity: U128::from(swap_data.liquidity),
        tick: swap_data.tick,
    })
}

/// Decode swap event from log and convert to V3SwapWithIntermediateResult
///
/// This function:
/// 1. Decodes a log as a Swap event
/// 2. Extracts swap event data
/// 3. Calculates amount_in and amount_out based on swap direction
/// 4. Returns a V3SwapWithIntermediateResult with is_to_intermediate flag
///
/// # Arguments
///
/// * `log` - The log entry from a transaction receipt
/// * `tx_hash` - The transaction hash
/// * `is_to_b` - If true, swap is from token A to token B; if false, from token
///   B to token A
/// * `is_to_intermediate` - If true, this swap is going towards the
///   intermediate token (in a two-hop swap context)
///
/// # Returns
///
/// Returns a `V3SwapWithIntermediateResult` if the log is a valid Swap event,
/// or an error otherwise.
#[instrument(skip(log), fields(
    log_address = ?log.address(),
    tx_hash = ?tx_hash,
    is_to_b = is_to_b,
    is_to_intermediate = is_to_intermediate
))]
pub fn decode_swap_event_to_result_with_intermediate(
    log: &Log,
    tx_hash: TxHash,
    is_to_b: bool,
    is_to_intermediate: bool,
    currency0: Currency,
    currency1: Currency,
) -> Result<V3SwapWithIntermediateResult> {
    let decoded = log.log_decode::<Swap>()?;
    let swap_data = SwapEventData {
        sender: decoded.inner.sender,
        recipient: decoded.inner.recipient,
        amount0: decoded.inner.amount0,
        amount1: decoded.inner.amount1,
        sqrt_price_x96: decoded.inner.sqrtPriceX96,
        liquidity: decoded.inner.liquidity,
        tick: decoded.inner.tick,
    };

    let (amount_in, amount_out, currency_in, currency_out) = if is_to_b {
        (swap_data.amount0.unsigned_abs(), swap_data.amount1.unsigned_abs(), currency0, currency1)
    } else {
        (swap_data.amount1.unsigned_abs(), swap_data.amount0.unsigned_abs(), currency1, currency0)
    };

    Ok(V3SwapWithIntermediateResult {
        tx_hash,
        sender: swap_data.sender,
        recipient: swap_data.recipient,
        amount_in: CurrencyAmount::from_raw_amount(currency_in, amount_in.to_big_int())?,
        amount_out: CurrencyAmount::from_raw_amount(currency_out, amount_out.to_big_int())?,
        sqrt_price_x96: swap_data.sqrt_price_x96,
        liquidity: U128::from(swap_data.liquidity),
        tick: swap_data.tick,
        is_to_intermediate,
    })
}