#![expect(
clippy::print_stdout,
clippy::print_stderr,
clippy::doc_markdown,
clippy::too_many_lines,
clippy::cast_possible_truncation
)]
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::ExitCode;
use alloy::primitives::{Address, B256};
use degenbot::config::loader::load_process_config;
use degenbot::config::resolvers::{resolve_chain_id, resolve_node_request_uri, NodeOverrides};
use degenbot::investigation::capture::{
diff_state, emit_json, pool_spec, refresh_fixture, FetchedState,
};
use degenbot::investigation::chain_capture::{
scrape_v2_state, scrape_v3_state, scrape_v4_state, TickScan,
};
use degenbot::rpc::provider::AlloyProvider;
use serde_json::Value;
const DEFAULT_STATE_VIEW: &str = "0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227";
struct Args {
check: bool,
scan: ScanArg,
tick_lens: Option<Address>,
state_view: Address,
files: Vec<PathBuf>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ScanArg {
Lens,
Bitmap,
}
fn parse_addr(label: &str, raw: &str) -> Result<Address, String> {
raw.parse()
.map_err(|e| format!("{label} {raw:?} is not an address: {e}"))
}
fn parse_args() -> Result<Args, String> {
let mut args = Args {
check: false,
scan: ScanArg::Bitmap,
tick_lens: None,
state_view: parse_addr("--state-view", DEFAULT_STATE_VIEW)?,
files: Vec::new(),
};
let mut rest = std::env::args().skip(1);
while let Some(arg) = rest.next() {
let mut value = |flag: &str| {
rest.next()
.ok_or_else(|| format!("{flag} requires a value"))
};
match arg.as_str() {
"--check" => args.check = true,
"--scan" => {
args.scan = match value("--scan")?.as_str() {
"lens" => ScanArg::Lens,
"bitmap" => ScanArg::Bitmap,
other => return Err(format!("--scan {other:?} (expected `lens` or `bitmap`)")),
}
}
"--tick-lens" => {
let raw = value("--tick-lens")?;
args.tick_lens = Some(parse_addr("--tick-lens", &raw)?);
}
"--state-view" => {
let raw = value("--state-view")?;
args.state_view = parse_addr("--state-view", &raw)?;
}
flag if flag.starts_with('-') => {
return Err(format!("unknown flag {flag:?}"));
}
_ => args.files.push(PathBuf::from(arg)),
}
}
if args.files.is_empty() {
return Err(
"usage: capture_path_fixture [--check] [--scan lens|bitmap] \
[--tick-lens <addr>] [--state-view <addr>] <fixture.json>..."
.into(),
);
}
Ok(args)
}
#[tokio::main]
async fn main() -> ExitCode {
let args = match parse_args() {
Ok(args) => args,
Err(msg) => {
eprintln!("error: {msg}");
return ExitCode::from(2);
}
};
let scan = match (args.scan, args.tick_lens) {
(ScanArg::Lens, Some(lens)) => TickScan::Lens(lens),
(ScanArg::Lens, None) => {
eprintln!("error: --scan lens requires --tick-lens <address>");
return ExitCode::from(2);
}
(ScanArg::Bitmap, Some(_)) => {
eprintln!("error: --tick-lens requires --scan lens");
return ExitCode::from(2);
}
(ScanArg::Bitmap, None) => TickScan::DirectPool,
};
let cfg = match load_process_config() {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("error: config: {e}");
return ExitCode::FAILURE;
}
};
let chain_id = match resolve_chain_id(&cfg, None) {
Ok(r) => r.value,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let node_uri = match resolve_node_request_uri(&cfg, chain_id, &NodeOverrides::new()) {
Ok(r) => r.value,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let provider = match AlloyProvider::new(&node_uri, 3).await {
Ok(provider) => provider,
Err(e) => {
eprintln!("error: node {node_uri:?}: {e}");
return ExitCode::FAILURE;
}
};
let mut failed = false;
for file in &args.files {
let text = match std::fs::read_to_string(file) {
Ok(text) => text,
Err(e) => {
eprintln!("error: {}: {e}", file.display());
failed = true;
continue;
}
};
let doc: Value = match serde_json::from_str(&text) {
Ok(doc) => doc,
Err(e) => {
eprintln!("error: {}: {e}", file.display());
failed = true;
continue;
}
};
let Some(target_block) = doc["target_block"].as_u64() else {
eprintln!("error: {}: missing target_block", file.display());
failed = true;
continue;
};
let mut fetched: BTreeMap<String, FetchedState> = BTreeMap::new();
let mut file_failed = false;
for (key, entry) in doc["pools"].as_object().into_iter().flatten() {
let spec = match pool_spec(entry) {
Ok(spec) => spec,
Err(e) => {
eprintln!("error: {}: pool `{key}`: {e}", file.display());
file_failed = true;
break;
}
};
let spacing = entry["tick_spacing"].as_i64().unwrap_or(1) as i32;
let state = if spec.family.ends_with("_v4") {
scrape_v4_state(
&provider,
args.state_view,
spec.pool_id.unwrap_or(B256::ZERO),
spacing,
target_block,
)
.await
} else if let Some(pool) = spec.address {
if spec.family.ends_with("_v3") {
scrape_v3_state(&provider, pool, spacing, target_block, scan).await
} else {
scrape_v2_state(&provider, pool, target_block).await
}
} else {
Err(format!("pool `{key}`: no address/pool_id in fixture"))
};
match state {
Ok(state) => {
fetched.insert(key.clone(), state);
}
Err(msg) => {
eprintln!("error: {}: {msg}", file.display());
file_failed = true;
break;
}
}
}
if file_failed {
failed = true;
continue;
}
let refreshed = match refresh_fixture(&doc, &fetched) {
Ok(doc) => doc,
Err(e) => {
eprintln!("error: {}: {e}", file.display());
failed = true;
continue;
}
};
if args.check {
let drift = diff_state(&doc, &refreshed);
if drift.is_empty() {
println!("ok {}", file.display());
} else {
println!("DRIFT {}", file.display());
for path in &drift {
let note = if path.ends_with("liquidity_update_block") {
" (snapshot-marker normalization: chain snapshots are exact at target_block)"
} else {
""
};
println!(" {path}{note}");
}
failed = true;
}
} else {
match std::fs::write(file, emit_json(&refreshed)) {
Ok(()) => println!("wrote {}", file.display()),
Err(e) => {
eprintln!("error: {}: {e}", file.display());
failed = true;
}
}
}
}
if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}