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,
},
};
#[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>> {
let positions = self.get_positions(token_ids).await.context("Failed to get positions")?;
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"));
}
let mut calldatas: Vec<Bytes> = Vec::with_capacity(params.token_ids.len());
for token_id in ¶ms.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, ¶ms.token_ids)
.await
.context("Failed to decode harvest results")?;
Ok(HarvestFeesAndRewardsResult { results, tx_hash: receipt.transaction_hash, receipt })
}
}