wp-evm-v3-core 0.1.1

Pure data + quote + plan for v3-family (CL) DEXes — no async dependencies
Documentation
//! Pure data records for the v3 family.
//!
//! These are dumb structs with no methods. All operations on them live
//! in `quote`, `plan`, or `hydrate` modules.

use alloy_primitives::{Address, B256, U256};

/// Snapshot of a v3 pool's on-chain state at some block.
///
/// Hydrated by `hydrate::pool_state`. Consumed by `quote::*` and `plan::*`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PoolState {
    pub token0: Address,
    pub token1: Address,
    pub fee: u32,
    pub tick_spacing: i32,
    pub sqrt_price_x96: U256,
    pub liquidity: u128,
    /// Current tick as reported by the pool's `slot0()` at hydration time.
    ///
    /// **Informational only.** Quote functions derive `tick_current` from
    /// `sqrt_price_x96` via the SDK's tick math; this field is not consumed
    /// by `quote::*` or `plan::*`. Stored for display and debugging.
    pub tick: i32,
    /// Initialized ticks within a window around the current tick. The
    /// window size is determined at hydrate time.
    ///
    /// **Sorted ascending by `tick`.** Callers must populate via
    /// `wp-evm-v3-provider::populate_ticks` (or equivalent) before passing
    /// the `PoolState` to `quote::*`. The native swap loop
    /// (`crate::swap::swap`) relies on this ordering for O(log n) tick
    /// search.
    pub ticks: Vec<TickInfo>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TickInfo {
    pub tick: i32,
    pub liquidity_net: i128,
    pub liquidity_gross: u128,
}

/// Snapshot of a v3 LP position.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PositionState {
    pub token_id: U256,
    pub owner: Address,
    pub token0: Address,
    pub token1: Address,
    pub fee: u32,
    pub tick_lower: i32,
    pub tick_upper: i32,
    pub liquidity: u128,
    pub fees_owed_0: U256,
    pub fees_owed_1: U256,
}

/// Per-protocol configuration baked into protocol facade crates.
///
/// Adding a new v3-fork DEX = creating a new protocol facade with a
/// `pub const CONFIG: V3ProtocolConfig = ...`. No family code changes.
///
/// Uniswap V3 SwapRouter ABI version. Determines whether the
/// `exactInputSingle` / `exactOutputSingle` params struct carries a
/// `deadline: uint256` field.
///
/// - `V1`: original SwapRouter (with deadline). Selector for
///   `exactInputSingle`: `0x414bf389`. Runtime size ~12,070 bytes.
/// - `V02`: SwapRouter02 (no `deadline`; checks tx-level deadline
///   externally via `Multicall` deadline wrapper if needed). Selector
///   for `exactInputSingle`: `0x04e45aaf`. Runtime size ~24,497 bytes.
///
/// All 8 V3-family routers in waterpump-evm scanned 2026-05-12 — see
/// R26-future-1 spec PR #200 §4 Slice 2 Task 6 amendment table for
/// per-facade-chain mapping. Base + Avalanche Uniswap V3 = V02; all
/// other 6 facade-chain combos = V1.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SwapRouterKind {
    /// Canonical Uniswap V3 SwapRouter (with `deadline` in params).
    V1,
    /// SwapRouter02 (no `deadline` in params).
    V02,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V3ProtocolConfig {
    pub factory: Address,
    /// CREATE2 caller for pool deployment. `None` means the factory
    /// itself is the deployer (Uniswap V3 pattern). `Some(addr)` routes
    /// CREATE2 derivation through a separate `PoolDeployer` contract
    /// (PancakeSwap V3 pattern; analogous to Algebra's `AlgebraPoolDeployer`).
    pub pool_deployer: Option<Address>,
    pub router: Address,
    /// SwapRouter ABI version — determines `exactInputSingle` /
    /// `exactOutputSingle` encoding shape (V1 includes `deadline`,
    /// V02 doesn't). See `SwapRouterKind` doc-comment for selector
    /// + runtime-size scan provenance.
    pub swap_router_kind: SwapRouterKind,
    pub position_mgr: Address,
    pub init_code_hash: B256,
    pub fee_tiers: &'static [u32],
    pub multicall: Address,
    /// Canonical QuoterV2 deployment for this protocol, when one exists.
    /// `None` means the facade has no on-chain parity oracle registered.
    /// `wp-evm-v3-provider::quote_online` returns an error if called with
    /// `None`.
    pub quoter: Option<Address>,
}

/// Parameters for an `exact_in` swap quote/plan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExactInParams {
    pub token_in: Address,
    pub token_out: Address,
    pub amount_in: U256,
    pub recipient: Address,
}

/// Parameters for an `exact_out` swap quote/plan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExactOutParams {
    pub token_in: Address,
    pub token_out: Address,
    pub amount_out: U256,
    pub recipient: Address,
}

/// Output of a quote computation. Pure — no chain interaction needed
/// to produce one given a `PoolState`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Quote {
    pub amount_in: U256,
    pub amount_out: U256,
    pub sqrt_price_x96_after: U256,
    pub price_impact_bps: u16,
}

/// Parameters for an `add_liquidity` plan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddLiquidityParams {
    pub token0: Address,
    pub token1: Address,
    pub fee: u32,
    pub tick_lower: i32,
    pub tick_upper: i32,
    pub amount0_desired: U256,
    pub amount1_desired: U256,
    pub recipient: Address,
}

/// Parameters for a `remove_liquidity` plan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoveLiquidityParams {
    pub token_id: U256,
    pub liquidity: u128,
    /// Minimum `token0` amount the caller expects back, computed off-chain
    /// from a prior quote against the current pool state. `None` means
    /// no slippage protection (zero) — only safe for private mempools or
    /// when the caller has other guarantees.
    pub amount0_min: Option<U256>,
    /// Same for `token1`.
    pub amount1_min: Option<U256>,
}

/// Parameters for a `collect_fees` plan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CollectFeesParams {
    pub token_id: U256,
    pub recipient: Address,
    /// Pool's `token0` address — caller-supplied (typically fetched
    /// via NFPM.positions(tokenId) before constructing this struct).
    /// Used by facade-layer native unwrap to compose `sweepToken` for
    /// the non-native side when `recipient == Address::ZERO`. Ignored
    /// for non-native recipient cases.
    pub token0: Address,
    /// Same for token1.
    pub token1: Address,
    /// Caller's address (msg.sender). When `recipient == ZERO` (native
    /// unwrap sentinel), used as the final `unwrapWETH9.recipient` and
    /// `sweepToken.recipient`. Ignored for non-native recipient cases.
    pub caller: Address,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoveAndCollectParams {
    /// NFPM position id — single source of truth (no risk of mismatch
    /// between separate remove + collect param structs).
    pub token_id: U256,
    // -- decrease side --
    pub liquidity: u128,
    pub amount0_min: Option<U256>,
    pub amount1_min: Option<U256>,
    // -- collect side --
    /// Recipient for the collected tokens. Pass `Address::ZERO` to
    /// trigger native unwrap (wrapped-native side → unwrapped to caller;
    /// non-native side → swept to caller as ERC20).
    pub recipient: Address,
    /// Pool's `token0` — caller fetches via NFPM.positions(tokenId)
    /// before constructing. Used by facade-layer native unwrap to
    /// determine which side is wrapped-native and which side to sweep.
    pub token0: Address,
    /// Same for token1.
    pub token1: Address,
    /// Caller's `msg.sender`. When `recipient == ZERO`, used as final
    /// `unwrapWETH9.recipient` and `sweepToken.recipient`. Ignored for
    /// non-native cases.
    pub caller: Address,
}

/// Re-export of `wp_evm_base::types::PlanFragment`.
///
/// Canonical single definition lives in `waterpump-base`. All family crates
/// re-export from there so the Phase 5 composer can aggregate fragments from
/// multiple families into one `Vec<PlanFragment>`.
pub use wp_evm_base::types::PlanFragment;

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_primitives::{address, U256};

    #[test]
    fn pool_state_constructs() {
        let s = PoolState {
            token0: address!("0x0000000000000000000000000000000000000001"),
            token1: address!("0x0000000000000000000000000000000000000002"),
            fee: 3000,
            tick_spacing: 60,
            sqrt_price_x96: U256::from(1u64) << 96,
            liquidity: 0u128,
            tick: 0i32,
            ticks: vec![],
        };
        assert_eq!(s.fee, 3000);
    }

    #[test]
    fn plan_fragment_default_is_empty() {
        let f = PlanFragment::default();
        assert!(f.calls.is_empty());
        assert!(f.approvals.is_empty());
        assert_eq!(f.value, U256::ZERO);
    }
}