1use std::sync::Arc;
15
16use alloy::primitives::{address, Address};
17use degenbot_aave::updater::verify::cleanup_zero_balance_positions_on_conn;
18use degenbot_aave::{
19 activate_aave_market, deactivate_aave_market, run_aave_update, NoProgress, RunError,
20};
21use degenbot_db::{ops, DbError, DegenbotDb};
22
23use crate::block::{parse_to_block, resolve_to_block};
24use crate::cancel::CancelHandle;
25use crate::context::CliContext;
26use crate::error::CliError;
27use crate::prompt::{PromptPlan, Prompter};
28use crate::report::{
29 AavePositionLine, AaveReport, AaveUpdateEntry, AaveUpdateOutcome, DeactivateOutcome,
30};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct AaveDeployment {
35 pub chain_id: u64,
37 pub chain_label: &'static str,
39 pub market_name: &'static str,
43 pub pool_address_provider: Address,
45 pub gho_token_address: Address,
47}
48
49pub const AAVE_DEPLOYMENTS: &[AaveDeployment] = &[AaveDeployment {
51 chain_id: 1,
52 chain_label: "Ethereum",
53 market_name: "Aave Ethereum Market",
58 pool_address_provider: address!("2f39d218133AFaB8F2B819B1066c7E434Ad94E9e"),
59 gho_token_address: address!("40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f"),
60}];
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum AaveCommand {
65 Activate {
67 chain_id: u64,
69 },
70 Deactivate {
72 chain_id: u64,
74 market_name: String,
76 },
77 Update {
79 chunk_size: u64,
81 to_block: String,
83 verify_chunk: bool,
85 verify_all: bool,
87 verify_all_interval: u64,
89 stop_after_one_chunk: bool,
91 dry_run: bool,
93 enable_backup: bool,
95 },
96 PositionShow {
98 address: String,
100 market: String,
102 chain_id: u64,
104 },
105}
106
107impl AaveCommand {
108 #[must_use]
110 pub const fn prompt_plan(&self, _ctx: &CliContext<'_>) -> PromptPlan {
111 PromptPlan::None
112 }
113}
114
115pub fn resolve_aave_deployment(chain_id: u64) -> Result<&'static AaveDeployment, CliError> {
121 AAVE_DEPLOYMENTS
122 .iter()
123 .find(|d| d.chain_id == chain_id)
124 .ok_or_else(|| CliError::UnknownDeployment {
125 chain_id,
126 name: "ethereum_aave_v3".to_string(),
127 })
128}
129
130pub(crate) fn execute(
140 command: &AaveCommand,
141 ctx: &CliContext<'_>,
142 _prompter: &dyn Prompter,
143 cancel: &CancelHandle,
144) -> Result<AaveReport, CliError> {
145 match command {
146 AaveCommand::Activate { chain_id } => activate(ctx, *chain_id),
147 AaveCommand::Deactivate {
148 chain_id,
149 market_name,
150 } => deactivate(ctx, *chain_id, market_name),
151 AaveCommand::Update {
152 chunk_size,
153 to_block,
154 verify_chunk,
155 verify_all,
156 verify_all_interval,
157 stop_after_one_chunk,
158 dry_run,
159 enable_backup,
160 } => update(
161 ctx,
162 cancel,
163 &UpdateArgs {
164 chunk_size: *chunk_size,
165 to_block,
166 verify_chunk: *verify_chunk,
167 verify_all: *verify_all,
168 verify_all_interval: *verify_all_interval,
169 stop_after_one_chunk: *stop_after_one_chunk,
170 dry_run: *dry_run,
171 enable_backup: *enable_backup,
172 },
173 ),
174 AaveCommand::PositionShow {
175 address,
176 market,
177 chain_id,
178 } => position_show(ctx, address, market, *chain_id),
179 }
180}
181
182fn activate(ctx: &CliContext<'_>, chain_id: u64) -> Result<AaveReport, CliError> {
184 let deployment = resolve_aave_deployment(chain_id)?;
185 let database_path = ctx.database_path().value;
186 let rpc_url = ctx.node_http_uri_for(chain_id)?.value;
187 let chain = i64::try_from(chain_id)
188 .map_err(|_| CliError::InvalidArgument(format!("chain id {chain_id} is out of range")))?;
189 let result = activate_aave_market(
190 &database_path,
191 chain,
192 &deployment.pool_address_provider.to_checksum(None),
193 &deployment.gho_token_address.to_checksum(None),
194 &rpc_url,
195 )
196 .map_err(CliError::AaveUpdate)?;
197 tracing::info!(
198 chain_id,
199 market_id = result.market_id,
200 "activated Aave V3 market"
201 );
202 Ok(AaveReport::Activated {
203 chain_id,
204 chain_label: deployment.chain_label,
205 market_id: result.market_id,
206 market_name: result.market_name,
207 created: result.created,
208 })
209}
210
211fn deactivate(
213 ctx: &CliContext<'_>,
214 chain_id: u64,
215 market_name: &str,
216) -> Result<AaveReport, CliError> {
217 let database_path = ctx.database_path().value;
218 let chain = i64::try_from(chain_id)
219 .map_err(|_| CliError::InvalidArgument(format!("chain id {chain_id} is out of range")))?;
220 let market = {
221 let db = (DegenbotDb::open(&database_path)?).0;
222 db.fetch_aave_market_by_name(chain, market_name)?
223 };
224 let Some(market) = market else {
225 return Ok(AaveReport::Deactivated {
226 chain_id,
227 market_id: None,
228 outcome: DeactivateOutcome::NoEntry,
229 });
230 };
231 if !market.active {
232 return Ok(AaveReport::Deactivated {
233 chain_id,
234 market_id: Some(market.id),
235 outcome: DeactivateOutcome::AlreadyDeactivated,
236 });
237 }
238 deactivate_aave_market(&database_path, market.id).map_err(CliError::AaveUpdate)?;
239 tracing::info!(
240 chain_id,
241 market_id = market.id,
242 "deactivated Aave V3 market"
243 );
244 Ok(AaveReport::Deactivated {
245 chain_id,
246 market_id: Some(market.id),
247 outcome: DeactivateOutcome::Deactivated,
248 })
249}
250
251#[expect(clippy::struct_excessive_bools)]
253struct UpdateArgs<'a> {
254 chunk_size: u64,
255 to_block: &'a str,
256 verify_chunk: bool,
257 verify_all: bool,
258 verify_all_interval: u64,
259 stop_after_one_chunk: bool,
260 dry_run: bool,
261 enable_backup: bool,
262}
263
264#[expect(clippy::too_many_lines)]
266fn update(
267 ctx: &CliContext<'_>,
268 cancel: &CancelHandle,
269 args: &UpdateArgs<'_>,
270) -> Result<AaveReport, CliError> {
271 let database_path = ctx.database_path().value;
272 crate::registrations::ensure_supported_registrations(&database_path)?;
276 let markets = {
277 let db = (DegenbotDb::open(&database_path)?).0;
278 db.fetch_active_aave_markets()?
279 };
280 if markets.is_empty() {
281 return Err(CliError::NoActiveAaveMarkets);
282 }
283 let spec = parse_to_block(args.to_block)?;
284 let interval = if args.verify_all {
285 Some(args.verify_all_interval)
286 } else {
287 None
288 };
289 let max_chunks = if args.stop_after_one_chunk {
290 Some(1)
291 } else {
292 None
293 };
294 let mut entries: Vec<AaveUpdateEntry> = Vec::new();
295 let mut chain_ids: Vec<i64> = Vec::new();
296 for market in &markets {
297 if !chain_ids.contains(&market.chain_id) {
298 chain_ids.push(market.chain_id);
299 }
300 }
301 for chain in chain_ids {
302 if cancel.is_cancelled() {
303 break;
304 }
305 let chain_unsigned = u64::try_from(chain).map_err(|_| {
306 CliError::InvalidArgument(format!("market chain id {chain} is out of range"))
307 })?;
308 let rpc_url = ctx.node_http_uri_for(chain_unsigned)?.value;
309 let resolved = resolve_to_block(spec, &rpc_url)?;
310 for market in markets.iter().filter(|m| m.chain_id == chain) {
311 if cancel.is_cancelled() {
312 return Ok(AaveReport::Updated { entries });
313 }
314 let Some(last_update_block) = market.last_update_block else {
315 entries.push(AaveUpdateEntry {
316 chain_id: chain,
317 market_id: market.id,
318 market_name: market.name.clone(),
319 outcome: AaveUpdateOutcome::NeedsBootstrap,
320 });
321 continue;
322 };
323 if args.dry_run {
324 entries.push(AaveUpdateEntry {
325 chain_id: chain,
326 market_id: market.id,
327 market_name: market.name.clone(),
328 outcome: AaveUpdateOutcome::DryRun {
329 last_update_block,
330 to_block: resolved,
331 },
332 });
333 continue;
334 }
335 match run_aave_update(
336 &database_path,
337 chain,
338 market.id,
339 resolved,
340 args.chunk_size,
341 &rpc_url,
342 cancel.flag(),
343 Arc::new(NoProgress),
344 args.verify_chunk,
345 interval,
346 args.verify_all,
347 max_chunks,
348 ) {
349 Ok(report) => {
350 entries.push(AaveUpdateEntry {
351 chain_id: chain,
352 market_id: market.id,
353 market_name: market.name.clone(),
354 outcome: AaveUpdateOutcome::Advanced {
355 from_block: report.from_block,
356 to_block: report.to_block,
357 chunks_committed: report.chunks_committed,
358 total_events_applied: report.total_events_applied,
359 },
360 });
361 cleanup_zero_balance_positions(&database_path, market.id)?;
362 if args.enable_backup {
363 let backup = backup_for_block(&database_path, report.to_block)?;
364 tracing::info!(
365 chain_id = chain,
366 to_block = report.to_block,
367 backup = %backup.display(),
368 "created Aave database backup at block"
369 );
370 }
371 }
372 Err(RunError::Cancelled) => {
373 entries.push(AaveUpdateEntry {
374 chain_id: chain,
375 market_id: market.id,
376 market_name: market.name.clone(),
377 outcome: AaveUpdateOutcome::Cancelled,
378 });
379 return Ok(AaveReport::Updated { entries });
380 }
381 Err(err) => return Err(CliError::AaveUpdate(err)),
382 }
383 }
384 }
385 Ok(AaveReport::Updated { entries })
386}
387
388fn cleanup_zero_balance_positions(
390 database_path: &std::path::Path,
391 market_id: i64,
392) -> Result<(), CliError> {
393 let (db, _state) = DegenbotDb::open_for_writes(database_path)?;
394 let mut guard = db.lock();
395 let tx = guard.transaction().map_err(DbError::from)?;
396 cleanup_zero_balance_positions_on_conn(&tx, market_id)?;
397 tx.commit().map_err(DbError::from)?;
398 Ok(())
399}
400
401fn backup_for_block(
403 database_path: &std::path::Path,
404 to_block: u64,
405) -> Result<std::path::PathBuf, CliError> {
406 let stem = database_path
407 .file_stem()
408 .map(|s| s.to_string_lossy().into_owned())
409 .unwrap_or_default();
410 let mut backup = database_path.to_path_buf();
411 backup.set_file_name(format!("{stem}-{to_block}.db.bak"));
412 ops::backup_database(database_path, &backup)?;
413 Ok(backup)
414}
415
416fn position_show(
418 ctx: &CliContext<'_>,
419 raw_address: &str,
420 market: &str,
421 chain_id: u64,
422) -> Result<AaveReport, CliError> {
423 let address: Address = raw_address
424 .parse()
425 .map_err(|_| CliError::InvalidAddress(raw_address.to_string()))?;
426 let user_address = address.to_checksum(None);
427 let chain = i64::try_from(chain_id)
428 .map_err(|_| CliError::InvalidArgument(format!("chain id {chain_id} is out of range")))?;
429 let database_path = ctx.database_path().value;
430 let db = (DegenbotDb::open(&database_path)?).0;
431 let Some(market_row) = db.fetch_aave_market_by_name(chain, market)? else {
432 return Ok(AaveReport::PositionNoMarket {
433 market: market.to_string(),
434 chain_id,
435 });
436 };
437 let Some(user) = db.fetch_aave_user_by_address(market_row.id, &user_address)? else {
438 return Ok(AaveReport::PositionNoUser {
439 user_address,
440 market: market.to_string(),
441 chain_id,
442 });
443 };
444 let collateral = db
445 .fetch_aave_collateral_positions(user.id)?
446 .into_iter()
447 .map(|p| AavePositionLine {
448 symbol: p.underlying_symbol.unwrap_or_else(|| "Unknown".to_string()),
449 balance: p.balance,
450 })
451 .collect();
452 let debt = db
453 .fetch_aave_debt_positions(user.id)?
454 .into_iter()
455 .map(|p| AavePositionLine {
456 symbol: p.underlying_symbol.unwrap_or_else(|| "Unknown".to_string()),
457 balance: p.balance,
458 })
459 .collect();
460 Ok(AaveReport::Position {
461 user_address,
462 market: market.to_string(),
463 chain_id,
464 collateral,
465 debt,
466 })
467}