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::{
    network::Ethereum,
    primitives::{Address, Bytes, U256},
    providers::DynProvider,
    rpc::types::TransactionReceipt,
};
use alloy_sol_types::SolCall;
use anyhow::{Context, Result};
use async_trait::async_trait;
use tracing::{debug, info, instrument};
use uniswap_sdk_core::prelude::{BigInt, CurrencyAmount, ToBig};
use uniswap_v3_sdk::prelude::encode_multicall;

use crate::{
    pool_swappers::common::{
        build_transaction_with_gas_prices, find_events, send_and_wait_for_transaction,
        MethodParameters,
    },
    traits::pool_harvester::{
        HarvestFeesAndRewardsParams, HarvestFeesAndRewardsResult, HarvestPositionResult,
        HarvestQuoteData, PoolHarvester,
    },
};

/// Quickswap Pool Harvester implementation.
#[derive(Clone)]
pub struct QuickswapPoolHarvester {
    pub position_manager_address: Address,
    pub sender_address: Address,
    pub chain_id: u64,
    pub provider: DynProvider<Ethereum>,
}

impl QuickswapPoolHarvester {
    pub fn new(
        position_manager_address: Address,
        sender_address: Address,
        chain_id: u64,
        provider: DynProvider<Ethereum>,
    ) -> Self {
        Self { position_manager_address, sender_address, chain_id, provider }
    }

    fn position_manager(&self) -> waterpump_evm_algebra_client::interfaces::INonfungiblePositionManager::INonfungiblePositionManagerInstance<
        DynProvider<Ethereum>,
        Ethereum,
    >{
        waterpump_evm_algebra_client::interfaces::INonfungiblePositionManager::INonfungiblePositionManagerInstance::new(
            self.position_manager_address,
            self.provider.clone(),
        )
    }

    async fn get_positions(
        &self,
        token_ids: &[U256],
    ) -> Result<
        Vec<waterpump_evm_algebra_client::interfaces::INonfungiblePositionManager::positionsReturn>,
    > {
        if token_ids.is_empty() {
            return Ok(Vec::new());
        }

        let position_manager = self.position_manager();
        let mut multicall = alloy::providers::Provider::multicall(&self.provider).dynamic();
        for token_id in token_ids {
            multicall = multicall.add_dynamic(position_manager.positions(*token_id));
        }
        Ok(multicall.aggregate().await?)
    }

    async fn decode_batch_collect_results(
        &self,
        receipt: &TransactionReceipt,
        token_ids: &[U256],
    ) -> Result<Vec<HarvestPositionResult>> {
        // Get token0/token1 addresses for all token_ids to determine currencies.
        let positions = self.get_positions(token_ids).await.context("Failed to get positions")?;

        // Find all Collect events from position manager.
        let collect_events: Vec<_> = find_events(receipt, |log, _| {
            if log.address() != self.position_manager_address {
                return Err(anyhow::anyhow!(
                    "Collect event from unexpected address: expected {:?}, got {:?}",
                    self.position_manager_address,
                    log.address()
                ));
            }

            log.log_decode::<waterpump_evm_algebra_client::interfaces::INonfungiblePositionManager::Collect>()
                .map(|decoded| decoded.inner)
                .map_err(|e| anyhow::anyhow!("Not a Collect event: {:?}", e))
        })
        .context("Failed to find Collect events in transaction receipt")?;

        debug!(
            collect_events_count = collect_events.len(),
            token_ids_count = token_ids.len(),
            "Decoded Collect events"
        );

        use std::collections::HashMap;
        let event_map: HashMap<U256, _> =
            collect_events.iter().map(|event| (event.tokenId, event)).collect();

        let mut results = Vec::with_capacity(token_ids.len());
        for (token_id, position_data) in token_ids.iter().zip(positions.iter()) {
            let currencies = waterpump_evm_uniswap_v3_client::get_currencies(
                &self.provider,
                &[position_data.token0, position_data.token1],
                self.chain_id,
            )
            .await
            .context("Failed to get currencies for decoding")?;

            let token0 = currencies.first().context("Expected token0 currency")?.clone();
            let token1 = currencies.get(1).context("Expected token1 currency")?.clone();

            if let Some(event) = event_map.get(token_id) {
                let amount0 = CurrencyAmount::from_raw_amount(
                    token0.clone(),
                    U256::from(event.amount0).to_big_int(),
                )
                .context("Failed to create CurrencyAmount for token0")?;
                let amount1 = CurrencyAmount::from_raw_amount(
                    token1.clone(),
                    U256::from(event.amount1).to_big_int(),
                )
                .context("Failed to create CurrencyAmount for token1")?;

                results.push(HarvestPositionResult {
                    token_id: *token_id,
                    amount0,
                    amount1,
                    reward_amounts: None,
                });
            } else {
                results.push(HarvestPositionResult {
                    token_id: *token_id,
                    amount0: CurrencyAmount::from_raw_amount(token0, BigInt::from(0))
                        .context("Failed to create empty CurrencyAmount for token0")?,
                    amount1: CurrencyAmount::from_raw_amount(token1, BigInt::from(0))
                        .context("Failed to create empty CurrencyAmount for token1")?,
                    reward_amounts: None,
                });
            }
        }

        Ok(results)
    }
}

#[async_trait]
impl PoolHarvester for QuickswapPoolHarvester {
    #[instrument(skip(self), fields(
        position_manager_address = ?self.position_manager_address,
        token_ids_count = token_ids.len()
    ))]
    async fn get_quote_data(&self, token_ids: &[U256]) -> Result<Vec<HarvestQuoteData>> {
        info!("Getting quote data for {} positions", token_ids.len());
        if token_ids.is_empty() {
            return Ok(Vec::new());
        }

        let positions = self.get_positions(token_ids).await.context("Failed to get positions")?;

        let collect_results = waterpump_evm_uniswap_v3_client::collect_multiple(
            &self.provider,
            self.position_manager_address,
            token_ids,
            None,
        )
        .await
        .context("Failed to collect quote data for positions")?;

        use std::collections::HashMap;
        let collect_map: HashMap<U256, &waterpump_evm_uniswap_v3_client::CollectResult> =
            collect_results.iter().map(|result| (result.token_id, result)).collect();

        let mut quote_data = Vec::with_capacity(token_ids.len());
        for (token_id, position_data) in token_ids.iter().zip(positions.iter()) {
            let currencies = waterpump_evm_uniswap_v3_client::get_currencies(
                &self.provider,
                &[position_data.token0, position_data.token1],
                self.chain_id,
            )
            .await
            .context("Failed to get currencies")?;

            let token0 = currencies.first().context("Expected token0 currency")?.clone();
            let token1 = currencies.get(1).context("Expected token1 currency")?.clone();

            let collect_result = collect_map
                .get(token_id)
                .context(format!("Missing collect result for token_id {}", token_id))?;

            let amount0 = CurrencyAmount::from_raw_amount(
                token0.clone(),
                collect_result.amount0.to_big_int(),
            )
            .context("Failed to create CurrencyAmount for token0")?;
            let amount1 = CurrencyAmount::from_raw_amount(
                token1.clone(),
                collect_result.amount1.to_big_int(),
            )
            .context("Failed to create CurrencyAmount for token1")?;

            quote_data.push(HarvestQuoteData {
                token_id: *token_id,
                amount0,
                amount1,
                reward_amounts: None,
            });
        }

        Ok(quote_data)
    }

    #[instrument(skip(self), fields(
        position_manager_address = ?self.position_manager_address,
        token_ids_count = params.token_ids.len(),
        recipient = ?params.recipient
    ))]
    async fn harvest_fees_and_rewards(
        &self,
        params: HarvestFeesAndRewardsParams,
    ) -> Result<HarvestFeesAndRewardsResult> {
        info!("Harvesting fees and rewards for {} positions", params.token_ids.len());
        if params.token_ids.is_empty() {
            return Err(anyhow::anyhow!("No token IDs provided for harvest"));
        }

        // Build multicall calldata for batch collect. This assumes Quickswap position
        // manager supports Uniswap-style `multicall(bytes[])` encoding.
        let mut calldatas: Vec<Bytes> = Vec::with_capacity(params.token_ids.len());
        for token_id in &params.token_ids {
            let call = waterpump_evm_algebra_client::interfaces::INonfungiblePositionManager::collectCall {
                params: waterpump_evm_algebra_client::interfaces::INonfungiblePositionManager::CollectParams {
                    tokenId: *token_id,
                    recipient: params.recipient,
                    amount0Max: u128::MAX,
                    amount1Max: u128::MAX,
                },
            };
            calldatas.push(call.abi_encode().into());
        }

        let method_params =
            MethodParameters { calldata: encode_multicall(calldatas), value: U256::ZERO };
        debug!(
            calldata_len = method_params.calldata.len(),
            "Built Quickswap batch collect transaction"
        );

        let tx = build_transaction_with_gas_prices(
            &self.provider,
            self.sender_address,
            self.position_manager_address,
            method_params,
            None::<crate::types::swap_params::GasPriceOptions>,
        )
        .await?;

        let receipt = send_and_wait_for_transaction(
            &self.provider,
            tx,
            Some(std::time::Duration::from_secs(60)),
            None::<fn(Box<dyn std::fmt::Display + Send + Sync>) -> anyhow::Error>,
        )
        .await?;

        info!(
            tx_hash = ?receipt.transaction_hash,
            block_number = ?receipt.block_number,
            gas_used = ?receipt.gas_used,
            status = ?receipt.status(),
            "Harvest transaction confirmed"
        );

        if !receipt.status() {
            return Err(anyhow::anyhow!("Harvest transaction failed"));
        }

        let results = self
            .decode_batch_collect_results(&receipt, &params.token_ids)
            .await
            .context("Failed to decode harvest results")?;

        Ok(HarvestFeesAndRewardsResult { results, tx_hash: receipt.transaction_hash, receipt })
    }
}