balq 0.2.2

BAL query: local archive of contract storage history, verified against block headers.
use super::Ctx;
use crate::ui;
use crate::util::emit;
use alloy_primitives::Address;
use anyhow::Result;
use bal_archive::{BackfillOpts, BackfillStop};
use bal_source::{Fallback, JsonRpcSource};
use serde_json::json;

pub struct Opts {
    pub address: Address,
    pub to: Option<u64>,
    pub resolve: bool,
    pub rpc: Option<String>,
    pub backup_rpc: Option<String>,
    pub chunk: u64,
}

fn stop_json(s: BackfillStop) -> serde_json::Value {
    match s {
        BackfillStop::Target => json!({ "kind": "target" }),
        BackfillStop::Creation(b) => json!({ "kind": "creation", "block": b }),
        BackfillStop::Resolved => json!({ "kind": "resolved" }),
        BackfillStop::Budget => json!({ "kind": "budget" }),
        BackfillStop::PreBal(b) => json!({ "kind": "preBal", "block": b }),
        BackfillStop::HistoryUnavailable(b) => {
            json!({ "kind": "historyUnavailable", "block": b })
        }
        BackfillStop::Nothing => json!({ "kind": "nothing" }),
    }
}

pub async fn run(ctx: &Ctx, o: Opts) -> Result<()> {
    let rpc = ctx.cfg.rpc(o.rpc)?;
    let backup = o.backup_rpc.or_else(|| ctx.cfg.backup_rpc.clone());
    let ar = ctx.open()?;
    let src = Fallback::new(JsonRpcSource::new(&rpc), backup.map(JsonRpcSource::new));

    let start = ar
        .watchlist()?
        .into_iter()
        .find(|(a, _)| *a == o.address)
        .map(|(_, s)| s)
        .unwrap_or(0);
    let total = o.to.map(|t| start.saturating_sub(t));
    let pb = (!ctx.json && !o.resolve).then(|| ui::walk_bar("backfill", total));
    let (mut scanned, mut records, mut resolved) = (0u64, 0usize, 0usize);
    let last = loop {
        let rep = ar
            .backfill(
                &src,
                o.address,
                BackfillOpts {
                    to: o.to,
                    max_blocks: Some(o.chunk.max(1)),
                    resolve_only: o.resolve,
                },
            )
            .await?;
        scanned += rep.blocks_scanned;
        records += rep.records_written;
        resolved += rep.slots_resolved;
        if let Some(pb) = &pb {
            pb.set_position(scanned);
            pb.set_message(ui::num(rep.to));
        }
        if rep.stopped != BackfillStop::Budget {
            break rep;
        }
    };
    if let Some(pb) = &pb {
        pb.finish_and_clear();
    }

    if ctx.json {
        emit(&json!({
            "address": o.address,
            "from": start, "to": last.to,
            "blocksScanned": scanned,
            "recordsWritten": records,
            "slotsResolved": resolved,
            "unresolved": last.unresolved,
            "createdAt": last.created_at,
            "stopped": stop_json(last.stopped),
        }));
        return Ok(());
    }
    let who = ui::bold(ui::short_addr(o.address));
    let tail = ui::dim(format!(
        "({} blocks read, {} records, {} earlier values found)",
        ui::num(scanned),
        ui::num(records as u64),
        resolved
    ));
    match last.stopped {
        BackfillStop::Creation(c) => ui::ok(format!(
            "{who}  created at {} — history complete, no value is unknown {tail}",
            ui::num(c)
        )),
        BackfillStop::Target => ui::ok(format!(
            "{who}  history now starts at {} {tail}",
            ui::num(last.to)
        )),
        BackfillStop::Resolved => ui::ok(format!(
            "{who}  every unknown earlier value found; history now starts at {} {tail}",
            ui::num(last.to)
        )),
        BackfillStop::Nothing => ui::ok(format!(
            "{who}  nothing to do (already at the target, or the deploy is known)"
        )),
        BackfillStop::PreBal(b) => ui::warn(format!(
            "{who}  block {} has no BAL hash (before the fork) — older state needs an archive proof {tail}",
            ui::num(b)
        )),
        BackfillStop::HistoryUnavailable(b) => ui::warn(format!(
            "{who}  the node does not serve block {} (history expiry?) — pass --backup-rpc with an endpoint that has it {tail}",
            ui::num(b)
        )),
        BackfillStop::Budget => ui::ok(format!("{who}  stopped at {} {tail}", ui::num(last.to))),
    }
    if last.unresolved > 0 && !matches!(last.stopped, BackfillStop::Creation(_)) {
        println!(
            "  {}",
            ui::dim(format!(
                "{} slot(s) still unknown before their first write — `balq backfill {}` without --resolve walks to the deploy",
                last.unresolved, o.address
            ))
        );
    }
    Ok(())
}