1use alloy_primitives::{Address, U256};
20use alloy_provider::{network::Ethereum, Provider};
21use alloy_sol_types::SolCall;
22use anyhow::{Context, Result};
23use wp_evm_base::{chain::Chain, evm::sign_extend_i24};
24use wp_evm_multicall::{aggregate3, IMulticall3};
25use wp_evm_v3_core::data::{PoolState, PositionState};
26use wp_evm_v3_interfaces::periphery::nfpm::INonfungiblePositionManagerView;
27use wp_evm_v3_interfaces::pool::{
28 immutables::IUniswapV3PoolImmutables, state::IUniswapV3PoolState,
29};
30
31#[tracing::instrument(skip_all, fields(pool = %pool), err)]
40pub async fn pool_state<P: Provider<Ethereum>>(
41 provider: &P,
42 multicall: Address,
43 pool: Address,
44) -> Result<PoolState> {
45 let mk = |data: Vec<u8>| IMulticall3::Call3 {
46 target: pool,
47 allowFailure: false,
48 callData: data.into(),
49 };
50
51 let calls = vec![
52 mk(IUniswapV3PoolImmutables::token0Call {}.abi_encode()),
53 mk(IUniswapV3PoolImmutables::token1Call {}.abi_encode()),
54 mk(IUniswapV3PoolImmutables::feeCall {}.abi_encode()),
55 mk(IUniswapV3PoolImmutables::tickSpacingCall {}.abi_encode()),
56 mk(IUniswapV3PoolState::liquidityCall {}.abi_encode()),
57 mk(IUniswapV3PoolState::slot0Call {}.abi_encode()),
58 ];
59
60 let raw = aggregate3(provider, multicall, calls).await?;
61 anyhow::ensure!(
62 raw.len() == 6,
63 "pool_state multicall returned {} results, expected 6",
64 raw.len(),
65 );
66
67 let token0 =
68 IUniswapV3PoolImmutables::token0Call::abi_decode_returns(raw[0].returnData.as_ref())
69 .context("decode token0")?;
70 let token1 =
71 IUniswapV3PoolImmutables::token1Call::abi_decode_returns(raw[1].returnData.as_ref())
72 .context("decode token1")?;
73 let fee = IUniswapV3PoolImmutables::feeCall::abi_decode_returns(raw[2].returnData.as_ref())
74 .context("decode fee")?;
75 let spacing =
76 IUniswapV3PoolImmutables::tickSpacingCall::abi_decode_returns(raw[3].returnData.as_ref())
77 .context("decode tickSpacing")?;
78 let liquidity =
79 IUniswapV3PoolState::liquidityCall::abi_decode_returns(raw[4].returnData.as_ref())
80 .context("decode liquidity")?;
81 let slot0 = IUniswapV3PoolState::slot0Call::abi_decode_returns(raw[5].returnData.as_ref())
82 .context("decode slot0")?;
83
84 Ok(PoolState {
85 token0,
86 token1,
87 fee: fee.to::<u32>(),
88 tick_spacing: sign_extend_i24(spacing),
89 sqrt_price_x96: U256::from(slot0.sqrtPriceX96),
90 liquidity,
91 tick: sign_extend_i24(slot0.tick),
92 ticks: vec![],
93 })
94}
95
96#[tracing::instrument(skip_all, fields(token_id = %token_id), err)]
106pub async fn position_state<P: Provider<Ethereum>>(
107 provider: &P,
108 multicall: Address,
109 position_manager: Address,
110 token_id: U256,
111) -> Result<PositionState> {
112 let mk = |data: Vec<u8>| IMulticall3::Call3 {
113 target: position_manager,
114 allowFailure: false,
115 callData: data.into(),
116 };
117
118 let calls = vec![
119 mk(INonfungiblePositionManagerView::positionsCall { tokenId: token_id }.abi_encode()),
120 mk(INonfungiblePositionManagerView::ownerOfCall { tokenId: token_id }.abi_encode()),
121 ];
122
123 let raw = aggregate3(provider, multicall, calls).await?;
124 anyhow::ensure!(
125 raw.len() == 2,
126 "position_state multicall returned {} results, expected 2",
127 raw.len(),
128 );
129
130 let pos = INonfungiblePositionManagerView::positionsCall::abi_decode_returns(
131 raw[0].returnData.as_ref(),
132 )
133 .context("decode positions")?;
134 let owner = INonfungiblePositionManagerView::ownerOfCall::abi_decode_returns(
135 raw[1].returnData.as_ref(),
136 )
137 .context("decode ownerOf")?;
138
139 Ok(PositionState {
140 token_id,
141 owner,
142 token0: pos.token0,
143 token1: pos.token1,
144 fee: pos.fee.to(),
145 tick_lower: sign_extend_i24(pos.tickLower),
146 tick_upper: sign_extend_i24(pos.tickUpper),
147 liquidity: pos.liquidity,
148 fees_owed_0: U256::from(pos.tokensOwed0),
149 fees_owed_1: U256::from(pos.tokensOwed1),
150 })
151}
152
153pub async fn position_token_pair<P: Provider<Ethereum>>(
157 provider: &P,
158 nfpm: Address,
159 token_id: U256,
160) -> Result<(Address, Address)> {
161 let ret = alloy_contract::SolCallBuilder::new_sol(
166 provider,
167 &nfpm,
168 &INonfungiblePositionManagerView::positionsCall { tokenId: token_id },
169 )
170 .call()
171 .await
172 .context("eth_call positions")?;
173 Ok((ret.token0, ret.token1))
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct Enumeration {
178 pub token_ids: Vec<U256>,
180 pub block_number: U256,
182 pub expected_count: u64,
184 pub failed_indices: Vec<u64>,
186}
187
188impl Enumeration {
189 fn empty(block_number: U256) -> Self {
190 Self { token_ids: Vec::new(), block_number, expected_count: 0, failed_indices: Vec::new() }
191 }
192}
193
194pub async fn enumerate_owner_token_ids<P: Provider<Ethereum>>(
202 provider: &P,
203 multicall: Address,
204 nfpm: Address,
205 owner: Address,
206 chain: Chain,
207) -> Result<Enumeration> {
208 let calls = vec![
209 IMulticall3::Call3 {
210 target: nfpm,
211 allowFailure: false,
212 callData: INonfungiblePositionManagerView::balanceOfCall { owner }.abi_encode().into(),
213 },
214 IMulticall3::Call3 {
215 target: multicall,
216 allowFailure: false,
217 callData: IMulticall3::getBlockNumberCall {}.abi_encode().into(),
218 },
219 ];
220
221 let raw = aggregate3(provider, multicall, calls).await?;
222 anyhow::ensure!(raw.len() == 2, "phase1 multicall returned {} results, expected 2", raw.len());
223
224 let balance = INonfungiblePositionManagerView::balanceOfCall::abi_decode_returns(
225 raw[0].returnData.as_ref(),
226 )
227 .context("decode balanceOf")?;
228 let block_number =
229 IMulticall3::getBlockNumberCall::abi_decode_returns(raw[1].returnData.as_ref())
230 .context("decode getBlockNumber")?;
231
232 let expected_count: u64 = balance.try_into().with_context(|| {
233 format!(
234 "balanceOf returned {balance} (exceeds u64) — does NFPM point at a real ERC-721? \
235 Chain: {chain}; NFPM: {nfpm:#x}",
236 )
237 })?;
238
239 if expected_count == 0 {
240 return Ok(Enumeration::empty(block_number));
241 }
242
243 let calls: Vec<IMulticall3::Call3> = (0..expected_count)
244 .map(|i| IMulticall3::Call3 {
245 target: nfpm,
246 allowFailure: true,
247 callData: INonfungiblePositionManagerView::tokenOfOwnerByIndexCall {
248 owner,
249 index: U256::from(i),
250 }
251 .abi_encode()
252 .into(),
253 })
254 .collect();
255
256 let raw = aggregate3(provider, multicall, calls).await?;
257 anyhow::ensure!(
258 raw.len() == expected_count as usize,
259 "phase2 multicall returned {} results, expected {}",
260 raw.len(),
261 expected_count,
262 );
263
264 let mut token_ids = Vec::with_capacity(raw.len());
265 let mut failed_indices = Vec::new();
266 for (idx, r) in raw.iter().enumerate() {
267 if !r.success {
268 failed_indices.push(idx as u64);
269 continue;
270 }
271 match INonfungiblePositionManagerView::tokenOfOwnerByIndexCall::abi_decode_returns(
272 r.returnData.as_ref(),
273 ) {
274 Ok(token_id) => token_ids.push(token_id),
275 Err(_) => failed_indices.push(idx as u64),
276 }
277 }
278
279 Ok(Enumeration { token_ids, block_number, expected_count, failed_indices })
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use alloy_primitives::{address, Bytes};
286 use alloy_sol_types::SolCall;
287
288 fn ok(return_data: Vec<u8>) -> IMulticall3::Result {
289 IMulticall3::Result { success: true, returnData: Bytes::from(return_data) }
290 }
291
292 fn failed() -> IMulticall3::Result {
293 IMulticall3::Result { success: false, returnData: Bytes::new() }
294 }
295
296 #[tokio::test]
297 async fn enumerate_reports_phase2_failed_indices() {
298 use alloy::transports::mock::Asserter;
299 use alloy_provider::ProviderBuilder;
300
301 let asserter = Asserter::new();
302 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
303 let multicall = address!("cA11bde05977b3631167028862bE2a173976CA11");
304 let nfpm = address!("00000000000000000000000000000000000000f1");
305 let owner = address!("00000000000000000000000000000000000000a1");
306
307 let phase1_returns = vec![
308 ok(INonfungiblePositionManagerView::balanceOfCall::abi_encode_returns(&U256::from(
309 3u64,
310 ))),
311 ok(IMulticall3::getBlockNumberCall::abi_encode_returns(&U256::from(123u64))),
312 ];
313 asserter.push_success(&IMulticall3::aggregate3Call::abi_encode_returns(&phase1_returns));
314
315 let phase2_returns = vec![
316 ok(INonfungiblePositionManagerView::tokenOfOwnerByIndexCall::abi_encode_returns(
317 &U256::from(11u64),
318 )),
319 ok(INonfungiblePositionManagerView::tokenOfOwnerByIndexCall::abi_encode_returns(
320 &U256::from(22u64),
321 )),
322 failed(),
323 ];
324 asserter.push_success(&IMulticall3::aggregate3Call::abi_encode_returns(&phase2_returns));
325
326 let enumeration =
327 enumerate_owner_token_ids(&provider, multicall, nfpm, owner, Chain::Ethereum)
328 .await
329 .expect("enumeration succeeds");
330
331 assert_eq!(enumeration.expected_count, 3);
332 assert_eq!(enumeration.block_number, U256::from(123u64));
333 assert_eq!(enumeration.token_ids, vec![U256::from(11u64), U256::from(22u64)]);
334 assert_eq!(enumeration.failed_indices, vec![2]);
335 }
336
337 #[tokio::test]
338 async fn enumerate_empty_wallet_short_circuits() {
339 use alloy::transports::mock::Asserter;
340 use alloy_provider::ProviderBuilder;
341
342 let asserter = Asserter::new();
343 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
344 let multicall = address!("cA11bde05977b3631167028862bE2a173976CA11");
345 let nfpm = address!("00000000000000000000000000000000000000f1");
346 let owner = address!("00000000000000000000000000000000000000a1");
347
348 let phase1_returns = vec![
349 ok(INonfungiblePositionManagerView::balanceOfCall::abi_encode_returns(&U256::ZERO)),
350 ok(IMulticall3::getBlockNumberCall::abi_encode_returns(&U256::from(456u64))),
351 ];
352 asserter.push_success(&IMulticall3::aggregate3Call::abi_encode_returns(&phase1_returns));
353
354 let enumeration =
355 enumerate_owner_token_ids(&provider, multicall, nfpm, owner, Chain::Ethereum)
356 .await
357 .expect("enumeration succeeds");
358
359 assert_eq!(enumeration.expected_count, 0);
360 assert_eq!(enumeration.block_number, U256::from(456u64));
361 assert!(enumeration.token_ids.is_empty());
362 assert!(enumeration.failed_indices.is_empty());
363 }
364
365 #[tokio::test]
366 async fn position_token_pair_decodes_v3_pair() {
367 use alloy::transports::mock::Asserter;
368 use alloy_provider::ProviderBuilder;
369 let ret = INonfungiblePositionManagerView::positionsReturn {
370 nonce: alloy_primitives::aliases::U96::ZERO,
371 operator: Address::ZERO,
372 token0: Address::with_last_byte(0xAA),
373 token1: Address::with_last_byte(0xBB),
374 fee: alloy_primitives::aliases::U24::from(500u32),
375 tickLower: alloy_primitives::aliases::I24::try_from(-100i32).unwrap(),
376 tickUpper: alloy_primitives::aliases::I24::try_from(100i32).unwrap(),
377 liquidity: 1u128,
378 feeGrowthInside0LastX128: U256::ZERO,
379 feeGrowthInside1LastX128: U256::ZERO,
380 tokensOwed0: 0u128,
381 tokensOwed1: 0u128,
382 };
383 let asserter = Asserter::new();
384 asserter.push_success(&alloy_primitives::Bytes::from(
385 INonfungiblePositionManagerView::positionsCall::abi_encode_returns(&ret),
386 ));
387 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
388 let (t0, t1) =
389 position_token_pair(&provider, Address::ZERO, U256::from(1u64)).await.unwrap();
390 assert_eq!(t0, Address::with_last_byte(0xAA));
391 assert_eq!(t1, Address::with_last_byte(0xBB));
392 }
393
394 #[test]
395 fn nfpm_enumerable_selectors_match_across_families() {
396 use wp_evm_algebra_interfaces::periphery::nfpm::IAlgebraNonfungiblePositionManager;
397 use wp_evm_ramses_interfaces::periphery::nfpm::IRamsesNonfungiblePositionManager;
398 use wp_evm_velodrome_interfaces::periphery::nfpm::IVelodromeNonfungiblePositionManager;
399
400 assert_eq!(
401 INonfungiblePositionManagerView::balanceOfCall::SELECTOR,
402 IAlgebraNonfungiblePositionManager::balanceOfCall::SELECTOR,
403 );
404 assert_eq!(
405 INonfungiblePositionManagerView::balanceOfCall::SELECTOR,
406 IRamsesNonfungiblePositionManager::balanceOfCall::SELECTOR,
407 );
408 assert_eq!(
409 INonfungiblePositionManagerView::balanceOfCall::SELECTOR,
410 IVelodromeNonfungiblePositionManager::balanceOfCall::SELECTOR,
411 );
412 assert_eq!(
413 INonfungiblePositionManagerView::tokenOfOwnerByIndexCall::SELECTOR,
414 IAlgebraNonfungiblePositionManager::tokenOfOwnerByIndexCall::SELECTOR,
415 );
416 assert_eq!(
417 INonfungiblePositionManagerView::tokenOfOwnerByIndexCall::SELECTOR,
418 IRamsesNonfungiblePositionManager::tokenOfOwnerByIndexCall::SELECTOR,
419 );
420 assert_eq!(
421 INonfungiblePositionManagerView::tokenOfOwnerByIndexCall::SELECTOR,
422 IVelodromeNonfungiblePositionManager::tokenOfOwnerByIndexCall::SELECTOR,
423 );
424 }
425}