1use alloy::primitives::{address, Address};
22use degenbot_db::DegenbotDb;
23use degenbot_uniswap::deployments;
24
25use crate::context::CliContext;
26use crate::error::CliError;
27use crate::prompt::{PromptPlan, Prompter};
28use crate::report::{
29 ActivateOutcome, DeactivateOutcome, ExchangeActiveState, ExchangeListRow, ExchangeReport,
30};
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ExchangeCommand {
35 Activate {
37 chain: String,
39 name: String,
41 },
42 Deactivate {
44 chain: String,
46 name: String,
48 },
49 List {
51 chain: Option<String>,
53 },
54}
55
56impl ExchangeCommand {
57 #[must_use]
59 pub const fn prompt_plan(&self, _ctx: &CliContext<'_>) -> PromptPlan {
60 PromptPlan::None
61 }
62
63 #[must_use]
66 pub fn selector(&self) -> Option<(&str, &str)> {
67 match self {
68 Self::Activate { chain, name } | Self::Deactivate { chain, name } => {
69 Some((chain, name))
70 }
71 Self::List { .. } => None,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct PoolManagerDeployment {
79 pub address: Address,
81 pub state_view: Address,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ExchangeDeployment {
88 pub chain_id: u64,
90 pub chain_label: &'static str,
92 pub dex_slug: &'static str,
94 pub display_name: &'static str,
96 pub factory: Address,
98 pub deployer: Option<Address>,
100 pub pool_manager: Option<PoolManagerDeployment>,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum DeploymentSource {
107 Registry {
109 factory: Address,
111 },
112 V4Singleton {
114 pool_manager: Address,
116 state_view: Address,
118 },
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct RetiredExchange {
124 pub chain_slug: &'static str,
126 pub chain_id: u64,
128 pub chain_label: &'static str,
130 pub dex_slug: &'static str,
132 pub display_name: &'static str,
134 pub source: DeploymentSource,
136}
137
138pub const RETIRED_EXCHANGES: &[RetiredExchange] = &[
142 RetiredExchange {
144 chain_slug: "base",
145 chain_id: 8453,
146 chain_label: "Base",
147 dex_slug: "aerodrome_v2",
148 display_name: "Aerodrome V2",
149 source: DeploymentSource::Registry {
150 factory: address!("420DD381b31aEf6683db6B902084cB0FFECe40Da"),
151 },
152 },
153 RetiredExchange {
154 chain_slug: "base",
155 chain_id: 8453,
156 chain_label: "Base",
157 dex_slug: "aerodrome_v3",
158 display_name: "Aerodrome V3",
159 source: DeploymentSource::Registry {
160 factory: address!("5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"),
161 },
162 },
163 RetiredExchange {
164 chain_slug: "base",
165 chain_id: 8453,
166 chain_label: "Base",
167 dex_slug: "pancakeswap_v2",
168 display_name: "Pancakeswap V2",
169 source: DeploymentSource::Registry {
170 factory: address!("02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E"),
171 },
172 },
173 RetiredExchange {
174 chain_slug: "base",
175 chain_id: 8453,
176 chain_label: "Base",
177 dex_slug: "pancakeswap_v3",
178 display_name: "Pancakeswap V3",
179 source: DeploymentSource::Registry {
180 factory: address!("0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"),
181 },
182 },
183 RetiredExchange {
184 chain_slug: "base",
185 chain_id: 8453,
186 chain_label: "Base",
187 dex_slug: "swapbased_v2",
188 display_name: "SwapBased V2",
189 source: DeploymentSource::Registry {
190 factory: address!("04C9f118d21e8B767D2e50C946f0cC9F6C367300"),
191 },
192 },
193 RetiredExchange {
194 chain_slug: "base",
195 chain_id: 8453,
196 chain_label: "Base",
197 dex_slug: "sushiswap_v2",
198 display_name: "Sushiswap V2",
199 source: DeploymentSource::Registry {
200 factory: address!("71524B4f93c58fcbF659783284E38825f0622859"),
201 },
202 },
203 RetiredExchange {
204 chain_slug: "base",
205 chain_id: 8453,
206 chain_label: "Base",
207 dex_slug: "sushiswap_v3",
208 display_name: "Sushiswap V3",
209 source: DeploymentSource::Registry {
210 factory: address!("c35DADB65012eC5796536bD9864eD8773aBc74C4"),
211 },
212 },
213 RetiredExchange {
214 chain_slug: "base",
215 chain_id: 8453,
216 chain_label: "Base",
217 dex_slug: "uniswap_v2",
218 display_name: "Uniswap V2",
219 source: DeploymentSource::Registry {
220 factory: address!("8909Dc15e40173Ff4699343b6eB8132c65e18eC6"),
221 },
222 },
223 RetiredExchange {
224 chain_slug: "base",
225 chain_id: 8453,
226 chain_label: "Base",
227 dex_slug: "uniswap_v3",
228 display_name: "Uniswap V3",
229 source: DeploymentSource::Registry {
230 factory: address!("33128a8fC17869897dcE68Ed026d694621f6FDfD"),
231 },
232 },
233 RetiredExchange {
234 chain_slug: "base",
235 chain_id: 8453,
236 chain_label: "Base",
237 dex_slug: "uniswap_v4",
238 display_name: "Uniswap V4",
239 source: DeploymentSource::V4Singleton {
240 pool_manager: address!("498581fF718922c3f8e6A244956aF099B2652b2b"),
241 state_view: address!("A3c0c9b65baD0b08107Aa264b0f3dB444b867A71"),
242 },
243 },
244 RetiredExchange {
246 chain_slug: "ethereum",
247 chain_id: 1,
248 chain_label: "Ethereum",
249 dex_slug: "pancakeswap_v2",
250 display_name: "Pancakeswap V2",
251 source: DeploymentSource::Registry {
252 factory: address!("1097053Fd2ea711dad45caCcc45EfF7548fCB362"),
253 },
254 },
255 RetiredExchange {
256 chain_slug: "ethereum",
257 chain_id: 1,
258 chain_label: "Ethereum",
259 dex_slug: "pancakeswap_v3",
260 display_name: "Pancakeswap V3",
261 source: DeploymentSource::Registry {
262 factory: address!("0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"),
263 },
264 },
265 RetiredExchange {
266 chain_slug: "ethereum",
267 chain_id: 1,
268 chain_label: "Ethereum",
269 dex_slug: "sushiswap_v2",
270 display_name: "Sushiswap V2",
271 source: DeploymentSource::Registry {
272 factory: address!("C0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac"),
273 },
274 },
275 RetiredExchange {
276 chain_slug: "ethereum",
277 chain_id: 1,
278 chain_label: "Ethereum",
279 dex_slug: "sushiswap_v3",
280 display_name: "Sushiswap V3",
281 source: DeploymentSource::Registry {
282 factory: address!("bACEB8eC6b9355Dfc0269C18bac9d6E2Bdc29C4F"),
283 },
284 },
285 RetiredExchange {
286 chain_slug: "ethereum",
287 chain_id: 1,
288 chain_label: "Ethereum",
289 dex_slug: "uniswap_v2",
290 display_name: "Uniswap V2",
291 source: DeploymentSource::Registry {
292 factory: address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"),
293 },
294 },
295 RetiredExchange {
296 chain_slug: "ethereum",
297 chain_id: 1,
298 chain_label: "Ethereum",
299 dex_slug: "uniswap_v3",
300 display_name: "Uniswap V3",
301 source: DeploymentSource::Registry {
302 factory: address!("1F98431c8aD98523631AE4a59f267346ea31F984"),
303 },
304 },
305 RetiredExchange {
306 chain_slug: "ethereum",
307 chain_id: 1,
308 chain_label: "Ethereum",
309 dex_slug: "uniswap_v4",
310 display_name: "Uniswap V4",
311 source: DeploymentSource::V4Singleton {
312 pool_manager: address!("000000000004444c5dc75cB358380D2e3dE08A90"),
313 state_view: address!("7fFE42C4a5DEeA5b0feC41C94C136Cf115597227"),
314 },
315 },
316];
317
318pub fn resolve_deployment(chain_id: u64, dex_slug: &str) -> Result<ExchangeDeployment, CliError> {
326 let entry = RETIRED_EXCHANGES
327 .iter()
328 .find(|e| e.chain_id == chain_id && e.dex_slug == dex_slug)
329 .ok_or_else(|| CliError::UnknownDeployment {
330 chain_id,
331 name: dex_slug.to_string(),
332 })?;
333 match entry.source {
334 DeploymentSource::Registry { factory } => {
335 let record = deployments::lookup(chain_id, factory).ok_or_else(|| {
336 CliError::UnknownDeployment {
337 chain_id,
338 name: dex_slug.to_string(),
339 }
340 })?;
341 Ok(ExchangeDeployment {
342 chain_id: entry.chain_id,
343 chain_label: entry.chain_label,
344 dex_slug: entry.dex_slug,
345 display_name: entry.display_name,
346 factory: record.factory,
347 deployer: record.deployer,
348 pool_manager: None,
349 })
350 }
351 DeploymentSource::V4Singleton {
352 pool_manager,
353 state_view,
354 } => Ok(ExchangeDeployment {
355 chain_id: entry.chain_id,
356 chain_label: entry.chain_label,
357 dex_slug: entry.dex_slug,
358 display_name: entry.display_name,
359 factory: pool_manager,
360 deployer: None,
361 pool_manager: Some(PoolManagerDeployment {
362 address: pool_manager,
363 state_view,
364 }),
365 }),
366 }
367}
368
369pub(crate) fn execute(
376 command: &ExchangeCommand,
377 ctx: &CliContext<'_>,
378 _prompter: &dyn Prompter,
379) -> Result<ExchangeReport, CliError> {
380 let path = ctx.database_path()?.value;
381 match command {
382 ExchangeCommand::Activate { chain, name } => activate(&deployment_for(chain, name)?, &path),
383 ExchangeCommand::Deactivate { chain, name } => {
384 deactivate(&deployment_for(chain, name)?, &path)
385 }
386 ExchangeCommand::List { chain } => list(chain.as_deref(), &path),
387 }
388}
389
390fn deployment_for(chain: &str, name: &str) -> Result<ExchangeDeployment, CliError> {
392 let chain_id = crate::block::resolve_chain_selector(chain)?;
393 resolve_deployment(chain_id, name)
394}
395
396fn list(chain_filter: Option<&str>, path: &std::path::Path) -> Result<ExchangeReport, CliError> {
399 let chain_id = chain_filter
400 .map(crate::block::resolve_chain_selector)
401 .transpose()?;
402 let (db, _state) = DegenbotDb::open(path)?;
403 let mut rows = Vec::new();
404 for entry in RETIRED_EXCHANGES {
405 if chain_id.is_some_and(|filtered| filtered != entry.chain_id) {
406 continue;
407 }
408 let row = db.fetch_exchange_by_name(
409 i64::try_from(entry.chain_id).map_err(|_| {
410 CliError::InvalidArgument(format!("chain id {} is out of range", entry.chain_id))
411 })?,
412 entry.dex_slug,
413 )?;
414 let state = match row {
415 Some(row) if row.active => ExchangeActiveState::Active,
416 Some(_) => ExchangeActiveState::Inactive,
417 None => ExchangeActiveState::NoEntry,
418 };
419 rows.push(ExchangeListRow {
420 chain_id: entry.chain_id,
421 chain_label: entry.chain_label,
422 display_name: entry.display_name,
423 dex_slug: entry.dex_slug,
424 factory: resolve_deployment(entry.chain_id, entry.dex_slug)?
425 .factory
426 .to_string(),
427 state,
428 });
429 }
430 Ok(ExchangeReport::Listed { rows })
431}
432
433fn activate(
435 deployment: &ExchangeDeployment,
436 path: &std::path::Path,
437) -> Result<ExchangeReport, CliError> {
438 let chain = i64::try_from(deployment.chain_id).map_err(|_| {
439 CliError::InvalidArgument(format!("chain id {} is out of range", deployment.chain_id))
440 })?;
441 let (db, _state) = DegenbotDb::open_for_writes(path)?;
442 let row = db.upsert_exchange(
443 chain,
444 deployment.dex_slug,
445 deployment.factory,
446 deployment.deployer,
447 )?;
448 if row.active {
449 return Ok(ExchangeReport::Activated {
450 chain_id: deployment.chain_id,
451 chain_label: deployment.chain_label,
452 display_name: deployment.display_name,
453 dex_slug: deployment.dex_slug,
454 outcome: ActivateOutcome::AlreadyActive,
455 });
456 }
457 db.set_exchange_active(row.id, true)?;
458 if let Some(manager) = deployment.pool_manager {
459 db.upsert_pool_manager(
460 manager.address,
461 chain,
462 deployment.dex_slug,
463 Some(manager.state_view),
464 row.id,
465 )?;
466 }
467 tracing::info!(
468 chain_id = deployment.chain_id,
469 name = deployment.dex_slug,
470 "activated exchange"
471 );
472 Ok(ExchangeReport::Activated {
473 chain_id: deployment.chain_id,
474 chain_label: deployment.chain_label,
475 display_name: deployment.display_name,
476 dex_slug: deployment.dex_slug,
477 outcome: ActivateOutcome::Activated,
478 })
479}
480
481fn deactivate(
483 deployment: &ExchangeDeployment,
484 path: &std::path::Path,
485) -> Result<ExchangeReport, CliError> {
486 let chain = i64::try_from(deployment.chain_id).map_err(|_| {
487 CliError::InvalidArgument(format!("chain id {} is out of range", deployment.chain_id))
488 })?;
489 let (db, _state) = DegenbotDb::open_for_writes(path)?;
490 let Some(row) = db.fetch_exchange_by_name(chain, deployment.dex_slug)? else {
491 return Ok(ExchangeReport::Deactivated {
492 chain_id: deployment.chain_id,
493 chain_label: deployment.chain_label,
494 display_name: deployment.display_name,
495 dex_slug: deployment.dex_slug,
496 outcome: DeactivateOutcome::NoEntry,
497 });
498 };
499 if !row.active {
500 return Ok(ExchangeReport::Deactivated {
501 chain_id: deployment.chain_id,
502 chain_label: deployment.chain_label,
503 display_name: deployment.display_name,
504 dex_slug: deployment.dex_slug,
505 outcome: DeactivateOutcome::AlreadyDeactivated,
506 });
507 }
508 db.set_exchange_active(row.id, false)?;
509 tracing::info!(
510 chain_id = deployment.chain_id,
511 name = deployment.dex_slug,
512 "deactivated exchange"
513 );
514 Ok(ExchangeReport::Deactivated {
515 chain_id: deployment.chain_id,
516 chain_label: deployment.chain_label,
517 display_name: deployment.display_name,
518 dex_slug: deployment.dex_slug,
519 outcome: DeactivateOutcome::Deactivated,
520 })
521}