1use alloy_primitives::{Address, U256};
8use alloy_provider::{network::Ethereum, Provider};
9use alloy_sol_types::SolCall;
10use anyhow::Result;
11use wp_evm_base::evm::sign_extend_i24;
12use wp_evm_multicall::{aggregate3, IMulticall3};
13use wp_evm_v3_interfaces::pool::{
14 immutables::IUniswapV3PoolImmutables, state::IUniswapV3PoolState,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct PoolViewData {
22 pub token0: Address,
23 pub token1: Address,
24 pub fee: u32,
25 pub is_dynamic_fee: bool,
26 pub tick_spacing: i32,
27 pub liquidity: u128,
28 pub sqrt_price_x96: U256,
29 pub tick: i32,
30}
31
32#[derive(Debug, Clone)]
35pub struct PoolReadEntry {
36 pub pool: Address,
37 pub result: std::result::Result<PoolViewData, String>,
38}
39
40pub trait PoolViewSource {
42 fn calls_per_pool(&self) -> usize;
44 fn pool_view_calls(&self, pool: Address) -> Vec<IMulticall3::Call3>;
46 fn decode_pool_view(&self, chunk: &[IMulticall3::Result]) -> Result<PoolViewData>;
48}
49
50pub async fn pool_views<P, S>(
52 provider: &P,
53 multicall: Address,
54 pools: &[Address],
55 source: &S,
56) -> Result<Vec<PoolReadEntry>>
57where
58 P: Provider<Ethereum>,
59 S: PoolViewSource,
60{
61 let per = source.calls_per_pool();
62 let mut calls: Vec<IMulticall3::Call3> = Vec::with_capacity(pools.len() * per);
63 for pool in pools {
64 calls.extend(source.pool_view_calls(*pool));
65 }
66 let raw = aggregate3(provider, multicall, calls).await?;
67 let expected = pools.len() * per;
68 anyhow::ensure!(
69 raw.len() == expected,
70 "multicall returned {} results for {} pools; expected {}",
71 raw.len(),
72 pools.len(),
73 expected,
74 );
75 Ok(pools
76 .iter()
77 .zip(raw.chunks(per))
78 .map(|(pool, chunk)| PoolReadEntry {
79 pool: *pool,
80 result: source.decode_pool_view(chunk).map_err(|e| e.to_string()),
81 })
82 .collect())
83}
84
85pub struct V3PoolViewSource;
87
88impl PoolViewSource for V3PoolViewSource {
89 fn calls_per_pool(&self) -> usize {
90 6
91 }
92
93 fn pool_view_calls(&self, pool: Address) -> Vec<IMulticall3::Call3> {
94 let mk = |data: Vec<u8>| IMulticall3::Call3 {
95 target: pool,
96 allowFailure: true,
97 callData: data.into(),
98 };
99 vec![
100 mk(IUniswapV3PoolImmutables::token0Call {}.abi_encode()),
101 mk(IUniswapV3PoolImmutables::token1Call {}.abi_encode()),
102 mk(IUniswapV3PoolImmutables::feeCall {}.abi_encode()),
103 mk(IUniswapV3PoolImmutables::tickSpacingCall {}.abi_encode()),
104 mk(IUniswapV3PoolState::liquidityCall {}.abi_encode()),
105 mk(IUniswapV3PoolState::slot0Call {}.abi_encode()),
106 ]
107 }
108
109 fn decode_pool_view(&self, chunk: &[IMulticall3::Result]) -> Result<PoolViewData> {
110 let need = |idx: usize, label: &str| -> Result<&[u8]> {
111 let r = &chunk[idx];
112 if !r.success {
113 anyhow::bail!("{label} reverted (pool may not be initialized or not a V3 pool)");
114 }
115 Ok(r.returnData.as_ref())
116 };
117 let token0 = IUniswapV3PoolImmutables::token0Call::abi_decode_returns(need(0, "token0")?)?;
118 let token1 = IUniswapV3PoolImmutables::token1Call::abi_decode_returns(need(1, "token1")?)?;
119 let fee = IUniswapV3PoolImmutables::feeCall::abi_decode_returns(need(2, "fee")?)?;
120 let tick_spacing =
121 IUniswapV3PoolImmutables::tickSpacingCall::abi_decode_returns(need(3, "tickSpacing")?)?;
122 let liquidity =
123 IUniswapV3PoolState::liquidityCall::abi_decode_returns(need(4, "liquidity")?)?;
124 let slot0 = IUniswapV3PoolState::slot0Call::abi_decode_returns(need(5, "slot0")?)?;
125 Ok(PoolViewData {
126 token0,
127 token1,
128 fee: fee.to::<u32>(),
129 is_dynamic_fee: false,
130 tick_spacing: sign_extend_i24(tick_spacing),
131 liquidity,
132 sqrt_price_x96: U256::from(slot0.sqrtPriceX96),
133 tick: sign_extend_i24(slot0.tick),
134 })
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use alloy_primitives::aliases::{I24, U160};
142
143 fn ok(data: Vec<u8>) -> IMulticall3::Result {
144 IMulticall3::Result { success: true, returnData: data.into() }
145 }
146
147 #[test]
148 fn v3_source_emits_6_calls() {
149 assert_eq!(V3PoolViewSource.calls_per_pool(), 6);
150 assert_eq!(V3PoolViewSource.pool_view_calls(Address::ZERO).len(), 6);
151 }
152
153 #[test]
154 fn v3_decode_happy_path() {
155 let t0 = Address::with_last_byte(0xAA);
156 let t1 = Address::with_last_byte(0xBB);
157 let chunk = [
158 ok(IUniswapV3PoolImmutables::token0Call::abi_encode_returns(&t0)),
159 ok(IUniswapV3PoolImmutables::token1Call::abi_encode_returns(&t1)),
160 ok(IUniswapV3PoolImmutables::feeCall::abi_encode_returns(
161 &alloy_primitives::aliases::U24::from(500u32),
162 )),
163 ok(IUniswapV3PoolImmutables::tickSpacingCall::abi_encode_returns(
164 &I24::try_from(10i32).unwrap(),
165 )),
166 ok(IUniswapV3PoolState::liquidityCall::abi_encode_returns(&123u128)),
167 ok(IUniswapV3PoolState::slot0Call::abi_encode_returns(
168 &IUniswapV3PoolState::slot0Return {
169 sqrtPriceX96: U160::from(2u64),
170 tick: I24::try_from(-42i32).unwrap(),
171 observationIndex: 0,
172 observationCardinality: 0,
173 observationCardinalityNext: 0,
174 feeProtocol: 0,
175 unlocked: true,
176 },
177 )),
178 ];
179 let d = V3PoolViewSource.decode_pool_view(&chunk).unwrap();
180 assert_eq!(d.token0, t0);
181 assert_eq!(d.fee, 500);
182 assert!(!d.is_dynamic_fee);
183 assert_eq!(d.tick_spacing, 10);
184 assert_eq!(d.tick, -42);
185 }
186
187 #[test]
188 fn v3_decode_revert_is_err_not_panic() {
189 let mut chunk: Vec<IMulticall3::Result> = (0..6)
190 .map(|_| IMulticall3::Result { success: true, returnData: Default::default() })
191 .collect();
192 chunk[0] = IMulticall3::Result { success: false, returnData: Default::default() };
193 let err = V3PoolViewSource.decode_pool_view(&chunk).unwrap_err().to_string();
194 assert_eq!(err, "token0 reverted (pool may not be initialized or not a V3 pool)");
195 }
196}