Skip to main content

degenbot_cli_core/
pool.rs

1//! The `pool` command arms (ADR-051 D1;).
2//!
3//! Ports `cli/pool.py` arm for arm:
4//!
5//! - `pool update` — the chunk-loop hand-off to
6//!   [`run_pool_update`](degenbot_pool_updater::run_pool_update), with the
7//!   `--chunk` / `--to-block` / `--verify-chunk` / `--verify-all` /
8//!   `--verify-all-interval` flags 1:1. Progress stays a no-op sink here
9//!   (ADR-051 D9: the bin paints it); a cooperative cancel returns a friendly
10//!   cancelled report (exit 0), matching the Python `RuntimeError` guard.
11//! - `pool verify` — the read-only, ad-hoc sibling: fetch the COMMITTED
12//!   liquidity map, compare against on-chain truth at `--block`, and render
13//!   GREEN / the named divergence list.
14
15use std::str::FromStr;
16use std::sync::Arc;
17
18use alloy::primitives::{Address, B256};
19use degenbot_db::{ComputedLiquidityUpdate, DegenbotDb};
20use degenbot_pool_updater::{
21    run_pool_update, verify_v3_liquidity_map_on_chain, verify_v4_liquidity_map_on_chain,
22    NoProgress, RunError,
23};
24
25use crate::block::{parse_to_block, resolve_to_block};
26use crate::cancel::CancelHandle;
27use crate::context::CliContext;
28use crate::error::CliError;
29use crate::prompt::{PromptPlan, Prompter};
30use crate::report::PoolReport;
31
32/// The RPC retry budget the verify arm's spot reads use.
33const RPC_MAX_RETRIES: u32 = 5;
34
35/// The pool family `--family` selects.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum PoolFamily {
38    /// V3 (`ticks()`/`tickBitmap()`).
39    V3,
40    /// V4 (`PoolManager` `extsload`).
41    V4,
42}
43
44impl PoolFamily {
45    /// The wire spelling (`v3` / `v4`).
46    #[must_use]
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Self::V3 => "v3",
50            Self::V4 => "v4",
51        }
52    }
53}
54
55/// The `pool` command group.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum PoolCommand {
58    /// Advance every active exchange's liquidity state to `to_block`.
59    Update {
60        /// Max blocks per chunk before committing.
61        chunk_size: u64,
62        /// The raw `--to-block` identifier (tag / tag:offset / integer).
63        to_block: String,
64        /// Run the pre-commit per-chunk on-chain-truth gate.
65        verify_chunk: bool,
66        /// Run the pre-commit market-wide verification at the interval + completion.
67        verify_all: bool,
68        /// The block interval for the `--verify-all` gate.
69        verify_all_interval: u64,
70    },
71    /// Verify a committed pool's liquidity map against on-chain truth.
72    Verify {
73        /// The HTTP RPC endpoint.
74        rpc_url: String,
75        /// The chain the pool lives on.
76        chain_id: i64,
77        /// The block number to read on-chain truth at.
78        block_number: u64,
79        /// V3 pool address, or V4 `PoolId` (bytes32 hex).
80        pool: String,
81        /// The pool family.
82        family: PoolFamily,
83        /// (V4 only) the `PoolManager` singleton.
84        pool_manager: Option<String>,
85    },
86}
87
88impl PoolCommand {
89    /// The arm's confirmation policy: neither pool arm prompts.
90    #[must_use]
91    pub const fn prompt_plan(&self, _ctx: &CliContext<'_>) -> PromptPlan {
92        PromptPlan::None
93    }
94}
95
96/// The resolved verify target for a family.
97enum VerifyTarget {
98    /// A V3 pool contract address.
99    V3(Address),
100    /// A V4 `PoolManager` + `PoolId`.
101    V4 { manager: Address, pool_id: B256 },
102}
103
104/// Execute a `pool` command.
105///
106/// # Errors
107///
108/// [`CliError::InvalidBlockTag`] / [`CliError::InvalidArgument`] /
109/// [`CliError::InvalidAddress`] for malformed inputs, [`CliError::Config`] for
110/// an unresolved driver-domain value, and [`CliError::PoolUpdate`] for a core
111/// failure.
112pub(crate) fn execute(
113    command: &PoolCommand,
114    ctx: &CliContext<'_>,
115    _prompter: &dyn Prompter,
116    cancel: &CancelHandle,
117) -> Result<PoolReport, CliError> {
118    match command {
119        PoolCommand::Update {
120            chunk_size,
121            to_block,
122            verify_chunk,
123            verify_all,
124            verify_all_interval,
125        } => update(
126            ctx,
127            cancel,
128            *chunk_size,
129            to_block,
130            *verify_chunk,
131            *verify_all,
132            *verify_all_interval,
133        ),
134        PoolCommand::Verify {
135            rpc_url,
136            chain_id,
137            block_number,
138            pool,
139            family,
140            pool_manager,
141        } => verify(
142            ctx,
143            rpc_url,
144            *chain_id,
145            *block_number,
146            pool,
147            *family,
148            pool_manager.as_deref(),
149        ),
150    }
151}
152
153/// `pool update`.
154fn update(
155    ctx: &CliContext<'_>,
156    cancel: &CancelHandle,
157    chunk_size: u64,
158    to_block: &str,
159    verify_chunk: bool,
160    verify_all: bool,
161    verify_all_interval: u64,
162) -> Result<PoolReport, CliError> {
163    let database_path = ctx.database_path().value;
164    let chain_id = ctx.chain_id()?.value;
165    let rpc_url = ctx.node_http_uri()?.value;
166    // Self-serve registration: every supported exchange pair not found in the
167    // DB registers inactive, so the update never depends on prior CREATEs.
168    crate::registrations::ensure_supported_registrations(&database_path)?;
169    let resolved = resolve_to_block(parse_to_block(to_block)?, &rpc_url)?;
170    let chain = i64::try_from(chain_id)
171        .map_err(|_| CliError::InvalidArgument(format!("chain id {chain_id} is out of range")))?;
172    let interval = if verify_all {
173        Some(verify_all_interval)
174    } else {
175        None
176    };
177    match run_pool_update(
178        &database_path,
179        chain,
180        resolved,
181        chunk_size,
182        &rpc_url,
183        cancel.flag(),
184        Arc::new(NoProgress),
185        verify_chunk,
186        interval,
187        verify_all,
188    ) {
189        Ok(report) => Ok(PoolReport::Updated {
190            chain_id: report.chain_id,
191            from_block: report.from_block,
192            to_block: report.to_block,
193            chunks_committed: report.chunks_committed,
194            total_pools_written: report.total_pools_written,
195            total_liquidity_applies: report.total_liquidity_applies,
196        }),
197        Err(RunError::Cancelled) => Ok(PoolReport::UpdateCancelled { chain_id: chain }),
198        Err(err) => Err(CliError::PoolUpdate(err)),
199    }
200}
201
202/// `pool verify`.
203fn verify(
204    ctx: &CliContext<'_>,
205    rpc_url: &str,
206    chain_id: i64,
207    block_number: u64,
208    pool: &str,
209    family: PoolFamily,
210    pool_manager: Option<&str>,
211) -> Result<PoolReport, CliError> {
212    let database_path = ctx.database_path().value;
213    let (computed, target) = {
214        let (db, _state) = DegenbotDb::open(&database_path)?;
215        let conn = db.lock();
216        fetch_verify_state(&conn, chain_id, pool, family, pool_manager)?
217    };
218    let divergences = match target {
219        VerifyTarget::V3(address) => crate::block::block_on(async {
220            let provider = degenbot_rpc::provider::AlloyProvider::new(rpc_url, RPC_MAX_RETRIES)
221                .await
222                .map_err(|err| CliError::BlockResolution(err.to_string()))?;
223            verify_v3_liquidity_map_on_chain(&provider, address, &computed, block_number)
224                .await
225                .map_err(CliError::PoolUpdate)
226        })??,
227        VerifyTarget::V4 { manager, pool_id } => crate::block::block_on(async {
228            let provider = degenbot_rpc::provider::AlloyProvider::new(rpc_url, RPC_MAX_RETRIES)
229                .await
230                .map_err(|err| CliError::BlockResolution(err.to_string()))?;
231            verify_v4_liquidity_map_on_chain(&provider, manager, pool_id, &computed, block_number)
232                .await
233                .map_err(CliError::PoolUpdate)
234        })??,
235    };
236    Ok(PoolReport::Verified {
237        pool: pool.to_string(),
238        family,
239        block_number,
240        divergences,
241    })
242}
243
244/// Fetch the pool's committed liquidity map + resolve the verify target.
245fn fetch_verify_state(
246    conn: &rusqlite::Connection,
247    chain_id: i64,
248    pool: &str,
249    family: PoolFamily,
250    pool_manager: Option<&str>,
251) -> Result<(ComputedLiquidityUpdate, VerifyTarget), CliError> {
252    match family {
253        PoolFamily::V3 => {
254            let address: Address = pool
255                .parse()
256                .map_err(|_| CliError::InvalidAddress(pool.to_string()))?;
257            let key = address.to_checksum(None);
258            let state = DegenbotDb::fetch_v3_pool_update_state_on_conn(conn, chain_id, &key)?
259                .ok_or_else(|| {
260                    CliError::InvalidArgument(format!(
261                        "v3 pool {key} not found on chain {chain_id}"
262                    ))
263                })?;
264            let (tick_bitmap, tick_data) =
265                DegenbotDb::fetch_v3_liquidity_map_on_conn(conn, state.pool_id)?;
266            Ok((
267                ComputedLiquidityUpdate {
268                    pool_id: state.pool_id,
269                    tick_spacing: state.tick_spacing,
270                    tick_data,
271                    tick_bitmap,
272                    last_event: None,
273                },
274                VerifyTarget::V3(address),
275            ))
276        }
277        PoolFamily::V4 => {
278            let manager = pool_manager.ok_or_else(|| {
279                CliError::InvalidArgument(
280                    "--pool-manager is required for --family v4 (the PoolManager singleton)."
281                        .to_string(),
282                )
283            })?;
284            let manager: Address = manager
285                .parse()
286                .map_err(|_| CliError::InvalidAddress(manager.to_string()))?;
287            let pool_id = B256::from_str(pool.strip_prefix("0x").unwrap_or(pool))
288                .map_err(|_| CliError::InvalidAddress(pool.to_string()))?;
289            let state = DegenbotDb::fetch_v4_pool_update_state_on_conn(conn, pool, chain_id)?
290                .ok_or_else(|| {
291                    CliError::InvalidArgument(format!(
292                        "v4 pool {pool} not found on chain {chain_id}"
293                    ))
294                })?;
295            let (tick_bitmap, tick_data) =
296                DegenbotDb::fetch_v4_liquidity_map_on_conn(conn, state.pool_id)?;
297            Ok((
298                ComputedLiquidityUpdate {
299                    pool_id: state.pool_id,
300                    tick_spacing: state.tick_spacing,
301                    tick_data,
302                    tick_bitmap,
303                    last_event: None,
304                },
305                VerifyTarget::V4 { manager, pool_id },
306            ))
307        }
308    }
309}