use alloy_primitives::{keccak256, Address, B256, U256};
use wp_evm_base::evm::sign_extend_i24;
use wp_evm_v3_interfaces::periphery::nfpm::INonfungiblePositionManagerView;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PositionView {
pub token_id: U256,
pub nonce: U256,
pub operator: Address,
pub token0: Address,
pub token1: Address,
pub fee: u32,
pub tick_lower: i32,
pub tick_upper: i32,
pub liquidity: u128,
pub fee_growth_inside_0_last_x128: U256,
pub fee_growth_inside_1_last_x128: U256,
pub tokens_owed_0_stale: u128,
pub tokens_owed_1_stale: u128,
}
impl PositionView {
pub fn from_nfpm_returns(
token_id: U256,
ret: &INonfungiblePositionManagerView::positionsReturn,
) -> Self {
Self {
token_id,
nonce: U256::from(ret.nonce),
operator: ret.operator,
token0: ret.token0,
token1: ret.token1,
fee: ret.fee.to(),
tick_lower: sign_extend_i24(ret.tickLower),
tick_upper: sign_extend_i24(ret.tickUpper),
liquidity: ret.liquidity,
fee_growth_inside_0_last_x128: ret.feeGrowthInside0LastX128,
fee_growth_inside_1_last_x128: ret.feeGrowthInside1LastX128,
tokens_owed_0_stale: ret.tokensOwed0,
tokens_owed_1_stale: ret.tokensOwed1,
}
}
}
pub fn position_key(owner: Address, tick_lower: i32, tick_upper: i32) -> B256 {
debug_assert!(
(-8_388_608..=8_388_607).contains(&tick_lower),
"tick_lower {} out of int24 range",
tick_lower
);
debug_assert!(
(-8_388_608..=8_388_607).contains(&tick_upper),
"tick_upper {} out of int24 range",
tick_upper
);
let mut buf = [0u8; 26];
buf[..20].copy_from_slice(owner.as_slice());
buf[20..23].copy_from_slice(&i24_to_be_bytes(tick_lower));
buf[23..26].copy_from_slice(&i24_to_be_bytes(tick_upper));
keccak256(buf)
}
fn i24_to_be_bytes(tick: i32) -> [u8; 3] {
let bytes = tick.to_be_bytes();
[bytes[1], bytes[2], bytes[3]]
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{address, b256};
#[test]
fn position_key_matches_solidity_reference() {
let owner: Address = address!("000000000000000000000000000000000000dEaD");
let tick_lower: i32 = -887_220;
let tick_upper: i32 = 887_220;
let key = position_key(owner, tick_lower, tick_upper);
let expected = b256!("1de7686f3203885e60772ba1129871b5a5d3c9b611ecdb79223ca2e8da545eb2");
assert_eq!(key, expected);
}
#[test]
fn position_key_is_order_sensitive() {
let owner: Address = address!("000000000000000000000000000000000000dEaD");
let key_normal = position_key(owner, -100, 100);
let key_swapped = position_key(owner, 100, -100);
assert_ne!(key_normal, key_swapped);
}
#[test]
fn position_view_round_trips_int24_negative_ticks() {
use alloy_primitives::aliases::{I24, U24, U96};
let token0: Address = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token1: Address = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let ret = INonfungiblePositionManagerView::positionsReturn {
nonce: U96::ZERO,
operator: Address::ZERO,
token0,
token1,
fee: U24::from(500u32),
tickLower: I24::try_from(-887_220i32).unwrap(),
tickUpper: I24::try_from(887_220i32).unwrap(),
liquidity: 1_000_000_000_000_000_000u128,
feeGrowthInside0LastX128: U256::from(42u64),
feeGrowthInside1LastX128: U256::from(99u64),
tokensOwed0: 0u128,
tokensOwed1: 0u128,
};
let view = PositionView::from_nfpm_returns(U256::from(12_345u64), &ret);
assert_eq!(view.tick_lower, -887_220);
assert_eq!(view.tick_upper, 887_220);
assert_eq!(view.fee, 500);
assert_eq!(view.token_id, U256::from(12_345u64));
assert_eq!(view.fee_growth_inside_0_last_x128, U256::from(42u64));
assert_eq!(view.fee_growth_inside_1_last_x128, U256::from(99u64));
}
#[test]
fn i24_to_be_bytes_handles_min_and_max() {
assert_eq!(i24_to_be_bytes(8_388_607), [0x7f, 0xff, 0xff]);
assert_eq!(i24_to_be_bytes(-8_388_608), [0x80, 0x00, 0x00]);
assert_eq!(i24_to_be_bytes(0), [0x00, 0x00, 0x00]);
assert_eq!(i24_to_be_bytes(-1), [0xff, 0xff, 0xff]);
}
}