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