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::{ActivateOutcome, DeactivateOutcome, ExchangeReport};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum ExchangeCommand {
33 Activate {
35 chain: String,
37 name: String,
39 },
40 Deactivate {
42 chain: String,
44 name: String,
46 },
47}
48
49impl ExchangeCommand {
50 #[must_use]
52 pub const fn prompt_plan(&self, _ctx: &CliContext<'_>) -> PromptPlan {
53 PromptPlan::None
54 }
55
56 #[must_use]
58 pub fn selector(&self) -> (&str, &str) {
59 match self {
60 Self::Activate { chain, name } | Self::Deactivate { chain, name } => (chain, name),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct PoolManagerDeployment {
68 pub address: Address,
70 pub state_view: Address,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ExchangeDeployment {
77 pub chain_id: u64,
79 pub chain_label: &'static str,
81 pub dex_slug: &'static str,
83 pub display_name: &'static str,
85 pub factory: Address,
87 pub deployer: Option<Address>,
89 pub pool_manager: Option<PoolManagerDeployment>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum DeploymentSource {
96 Registry {
98 factory: Address,
100 },
101 V4Singleton {
103 pool_manager: Address,
105 state_view: Address,
107 },
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct RetiredExchange {
113 pub chain_slug: &'static str,
115 pub chain_id: u64,
117 pub chain_label: &'static str,
119 pub dex_slug: &'static str,
121 pub display_name: &'static str,
123 pub source: DeploymentSource,
125}
126
127pub const RETIRED_EXCHANGES: &[RetiredExchange] = &[
131 RetiredExchange {
133 chain_slug: "base",
134 chain_id: 8453,
135 chain_label: "Base",
136 dex_slug: "aerodrome_v2",
137 display_name: "Aerodrome V2",
138 source: DeploymentSource::Registry {
139 factory: address!("420DD381b31aEf6683db6B902084cB0FFECe40Da"),
140 },
141 },
142 RetiredExchange {
143 chain_slug: "base",
144 chain_id: 8453,
145 chain_label: "Base",
146 dex_slug: "aerodrome_v3",
147 display_name: "Aerodrome V3",
148 source: DeploymentSource::Registry {
149 factory: address!("5e7BB104d84c7CB9B682AaC2F3d509f5F406809A"),
150 },
151 },
152 RetiredExchange {
153 chain_slug: "base",
154 chain_id: 8453,
155 chain_label: "Base",
156 dex_slug: "pancakeswap_v2",
157 display_name: "Pancakeswap V2",
158 source: DeploymentSource::Registry {
159 factory: address!("02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E"),
160 },
161 },
162 RetiredExchange {
163 chain_slug: "base",
164 chain_id: 8453,
165 chain_label: "Base",
166 dex_slug: "pancakeswap_v3",
167 display_name: "Pancakeswap V3",
168 source: DeploymentSource::Registry {
169 factory: address!("0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"),
170 },
171 },
172 RetiredExchange {
173 chain_slug: "base",
174 chain_id: 8453,
175 chain_label: "Base",
176 dex_slug: "swapbased_v2",
177 display_name: "SwapBased V2",
178 source: DeploymentSource::Registry {
179 factory: address!("04C9f118d21e8B767D2e50C946f0cC9F6C367300"),
180 },
181 },
182 RetiredExchange {
183 chain_slug: "base",
184 chain_id: 8453,
185 chain_label: "Base",
186 dex_slug: "sushiswap_v2",
187 display_name: "Sushiswap V2",
188 source: DeploymentSource::Registry {
189 factory: address!("71524B4f93c58fcbF659783284E38825f0622859"),
190 },
191 },
192 RetiredExchange {
193 chain_slug: "base",
194 chain_id: 8453,
195 chain_label: "Base",
196 dex_slug: "sushiswap_v3",
197 display_name: "Sushiswap V3",
198 source: DeploymentSource::Registry {
199 factory: address!("c35DADB65012eC5796536bD9864eD8773aBc74C4"),
200 },
201 },
202 RetiredExchange {
203 chain_slug: "base",
204 chain_id: 8453,
205 chain_label: "Base",
206 dex_slug: "uniswap_v2",
207 display_name: "Uniswap V2",
208 source: DeploymentSource::Registry {
209 factory: address!("8909Dc15e40173Ff4699343b6eB8132c65e18eC6"),
210 },
211 },
212 RetiredExchange {
213 chain_slug: "base",
214 chain_id: 8453,
215 chain_label: "Base",
216 dex_slug: "uniswap_v3",
217 display_name: "Uniswap V3",
218 source: DeploymentSource::Registry {
219 factory: address!("33128a8fC17869897dcE68Ed026d694621f6FDfD"),
220 },
221 },
222 RetiredExchange {
223 chain_slug: "base",
224 chain_id: 8453,
225 chain_label: "Base",
226 dex_slug: "uniswap_v4",
227 display_name: "Uniswap V4",
228 source: DeploymentSource::V4Singleton {
229 pool_manager: address!("498581fF718922c3f8e6A244956aF099B2652b2b"),
230 state_view: address!("A3c0c9b65baD0b08107Aa264b0f3dB444b867A71"),
231 },
232 },
233 RetiredExchange {
235 chain_slug: "ethereum",
236 chain_id: 1,
237 chain_label: "Ethereum",
238 dex_slug: "pancakeswap_v2",
239 display_name: "Pancakeswap V2",
240 source: DeploymentSource::Registry {
241 factory: address!("1097053Fd2ea711dad45caCcc45EfF7548fCB362"),
242 },
243 },
244 RetiredExchange {
245 chain_slug: "ethereum",
246 chain_id: 1,
247 chain_label: "Ethereum",
248 dex_slug: "pancakeswap_v3",
249 display_name: "Pancakeswap V3",
250 source: DeploymentSource::Registry {
251 factory: address!("0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865"),
252 },
253 },
254 RetiredExchange {
255 chain_slug: "ethereum",
256 chain_id: 1,
257 chain_label: "Ethereum",
258 dex_slug: "sushiswap_v2",
259 display_name: "Sushiswap V2",
260 source: DeploymentSource::Registry {
261 factory: address!("C0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac"),
262 },
263 },
264 RetiredExchange {
265 chain_slug: "ethereum",
266 chain_id: 1,
267 chain_label: "Ethereum",
268 dex_slug: "sushiswap_v3",
269 display_name: "Sushiswap V3",
270 source: DeploymentSource::Registry {
271 factory: address!("bACEB8eC6b9355Dfc0269C18bac9d6E2Bdc29C4F"),
272 },
273 },
274 RetiredExchange {
275 chain_slug: "ethereum",
276 chain_id: 1,
277 chain_label: "Ethereum",
278 dex_slug: "uniswap_v2",
279 display_name: "Uniswap V2",
280 source: DeploymentSource::Registry {
281 factory: address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"),
282 },
283 },
284 RetiredExchange {
285 chain_slug: "ethereum",
286 chain_id: 1,
287 chain_label: "Ethereum",
288 dex_slug: "uniswap_v3",
289 display_name: "Uniswap V3",
290 source: DeploymentSource::Registry {
291 factory: address!("1F98431c8aD98523631AE4a59f267346ea31F984"),
292 },
293 },
294 RetiredExchange {
295 chain_slug: "ethereum",
296 chain_id: 1,
297 chain_label: "Ethereum",
298 dex_slug: "uniswap_v4",
299 display_name: "Uniswap V4",
300 source: DeploymentSource::V4Singleton {
301 pool_manager: address!("000000000004444c5dc75cB358380D2e3dE08A90"),
302 state_view: address!("7fFE42C4a5DEeA5b0feC41C94C136Cf115597227"),
303 },
304 },
305];
306
307pub fn resolve_deployment(chain_id: u64, dex_slug: &str) -> Result<ExchangeDeployment, CliError> {
315 let entry = RETIRED_EXCHANGES
316 .iter()
317 .find(|e| e.chain_id == chain_id && e.dex_slug == dex_slug)
318 .ok_or_else(|| CliError::UnknownDeployment {
319 chain_id,
320 name: dex_slug.to_string(),
321 })?;
322 match entry.source {
323 DeploymentSource::Registry { factory } => {
324 let record = deployments::lookup(chain_id, factory).ok_or_else(|| {
325 CliError::UnknownDeployment {
326 chain_id,
327 name: dex_slug.to_string(),
328 }
329 })?;
330 Ok(ExchangeDeployment {
331 chain_id: entry.chain_id,
332 chain_label: entry.chain_label,
333 dex_slug: entry.dex_slug,
334 display_name: entry.display_name,
335 factory: record.factory,
336 deployer: record.deployer,
337 pool_manager: None,
338 })
339 }
340 DeploymentSource::V4Singleton {
341 pool_manager,
342 state_view,
343 } => Ok(ExchangeDeployment {
344 chain_id: entry.chain_id,
345 chain_label: entry.chain_label,
346 dex_slug: entry.dex_slug,
347 display_name: entry.display_name,
348 factory: pool_manager,
349 deployer: None,
350 pool_manager: Some(PoolManagerDeployment {
351 address: pool_manager,
352 state_view,
353 }),
354 }),
355 }
356}
357
358pub(crate) fn execute(
365 command: &ExchangeCommand,
366 ctx: &CliContext<'_>,
367 _prompter: &dyn Prompter,
368) -> Result<ExchangeReport, CliError> {
369 let (chain_selector, name) = command.selector();
370 let chain_id = crate::block::resolve_chain_selector(chain_selector)?;
371 let deployment = resolve_deployment(chain_id, name)?;
372 let path = ctx.database_path().value;
373 match command {
374 ExchangeCommand::Activate { .. } => activate(&deployment, &path),
375 ExchangeCommand::Deactivate { .. } => deactivate(&deployment, &path),
376 }
377}
378
379fn activate(
381 deployment: &ExchangeDeployment,
382 path: &std::path::Path,
383) -> Result<ExchangeReport, CliError> {
384 let chain = i64::try_from(deployment.chain_id).map_err(|_| {
385 CliError::InvalidArgument(format!("chain id {} is out of range", deployment.chain_id))
386 })?;
387 let (db, _state) = DegenbotDb::open_for_writes(path)?;
388 let row = db.upsert_exchange(
389 chain,
390 deployment.dex_slug,
391 deployment.factory,
392 deployment.deployer,
393 )?;
394 if row.active {
395 return Ok(ExchangeReport::Activated {
396 chain_id: deployment.chain_id,
397 chain_label: deployment.chain_label,
398 display_name: deployment.display_name,
399 dex_slug: deployment.dex_slug,
400 outcome: ActivateOutcome::AlreadyActive,
401 });
402 }
403 db.set_exchange_active(row.id, true)?;
404 if let Some(manager) = deployment.pool_manager {
405 db.upsert_pool_manager(
406 manager.address,
407 chain,
408 deployment.dex_slug,
409 Some(manager.state_view),
410 row.id,
411 )?;
412 }
413 tracing::info!(
414 chain_id = deployment.chain_id,
415 name = deployment.dex_slug,
416 "activated exchange"
417 );
418 Ok(ExchangeReport::Activated {
419 chain_id: deployment.chain_id,
420 chain_label: deployment.chain_label,
421 display_name: deployment.display_name,
422 dex_slug: deployment.dex_slug,
423 outcome: ActivateOutcome::Activated,
424 })
425}
426
427fn deactivate(
429 deployment: &ExchangeDeployment,
430 path: &std::path::Path,
431) -> Result<ExchangeReport, CliError> {
432 let chain = i64::try_from(deployment.chain_id).map_err(|_| {
433 CliError::InvalidArgument(format!("chain id {} is out of range", deployment.chain_id))
434 })?;
435 let (db, _state) = DegenbotDb::open_for_writes(path)?;
436 let Some(row) = db.fetch_exchange_by_name(chain, deployment.dex_slug)? else {
437 return Ok(ExchangeReport::Deactivated {
438 chain_id: deployment.chain_id,
439 chain_label: deployment.chain_label,
440 display_name: deployment.display_name,
441 dex_slug: deployment.dex_slug,
442 outcome: DeactivateOutcome::NoEntry,
443 });
444 };
445 if !row.active {
446 return Ok(ExchangeReport::Deactivated {
447 chain_id: deployment.chain_id,
448 chain_label: deployment.chain_label,
449 display_name: deployment.display_name,
450 dex_slug: deployment.dex_slug,
451 outcome: DeactivateOutcome::AlreadyDeactivated,
452 });
453 }
454 db.set_exchange_active(row.id, false)?;
455 tracing::info!(
456 chain_id = deployment.chain_id,
457 name = deployment.dex_slug,
458 "deactivated exchange"
459 );
460 Ok(ExchangeReport::Deactivated {
461 chain_id: deployment.chain_id,
462 chain_label: deployment.chain_label,
463 display_name: deployment.display_name,
464 dex_slug: deployment.dex_slug,
465 outcome: DeactivateOutcome::Deactivated,
466 })
467}