Skip to main content

wp_evm_v3_provider/
pool_views.rs

1//! Batch pool-view reader — the read+decode half of `wpe * pool view`,
2//! hoisted off the CLI (M4-A5). Mirrors `position_views.rs`: a generic
3//! engine owns the single `aggregate3`; each family supplies a decode-only
4//! `PoolViewSource` impl on a local unit struct (orphan-safe). Rendering
5//! stays in the CLI (per-family JSON key orders differ).
6
7use 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/// Normalized pool snapshot — the exact field set the CLI's `output::PoolView`
18/// renders. Families that have no fee-tier field (ramses) or recompute fee in
19/// the renderer (velodrome) set `fee: 0`; the CLI render path is unaffected.
20#[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/// One pool's read result — resilient per-entry (a single bad pool does not
33/// abort the batch).
34#[derive(Debug, Clone)]
35pub struct PoolReadEntry {
36    pub pool: Address,
37    pub result: std::result::Result<PoolViewData, String>,
38}
39
40/// Per-family pool decode seam. One impl per pool ABI on a local unit struct.
41pub trait PoolViewSource {
42    /// Calls emitted per pool — the multicall chunk size.
43    fn calls_per_pool(&self) -> usize;
44    /// Build the per-pool view calls.
45    fn pool_view_calls(&self, pool: Address) -> Vec<IMulticall3::Call3>;
46    /// Decode one pool's result chunk into normalized data.
47    fn decode_pool_view(&self, chunk: &[IMulticall3::Result]) -> Result<PoolViewData>;
48}
49
50/// Batch-read `pools` via a single Multicall3, decoding each via `source`.
51pub 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
85/// V3-fork pool decode (Uniswap / Pancake / Sushi / PRJX share this ABI).
86pub 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}