use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
use arete_mcp::dev_capture::{capture_from_rpc, parse_window, HttpRpc, RpcCaptureOptions};
use clap::Args;
use colored::Colorize;
use serde_json::Value;
use super::{block_on, print, project_root, ProjectArgs};
pub const DEFAULT_CAPTURE_RPC: &str = "https://api.mainnet-beta.solana.com";
#[derive(Args, Debug, Clone)]
pub struct CaptureArgs {
#[command(flatten)]
pub project: ProjectArgs,
#[arg(long = "program", value_name = "ID|ALIAS", required = true)]
pub programs: Vec<String>,
#[arg(long, allow_hyphen_values = true)]
pub from: String,
#[arg(long, allow_hyphen_values = true)]
pub to: Option<String>,
#[arg(long, env = "ARETE_CAPTURE_RPC_URL", default_value = DEFAULT_CAPTURE_RPC, value_name = "URL")]
pub rpc_url: String,
#[arg(long = "accounts", value_delimiter = ',', value_name = "PUBKEY")]
pub accounts: Vec<String>,
#[arg(long)]
pub no_touched_state: bool,
#[arg(long, default_value_t = 500)]
pub max_transactions: usize,
#[arg(long, default_value_t = 20)]
pub max_signature_pages: usize,
#[arg(long, default_value_t = 500)]
pub max_state_accounts: usize,
#[arg(long)]
pub no_block_order: bool,
#[arg(long, default_value_t = 8.0)]
pub rps: f64,
#[arg(short = 'o', long, value_name = "FILE")]
pub output: PathBuf,
}
fn is_address(text: &str) -> bool {
bs58::decode(text)
.into_vec()
.is_ok_and(|bytes| bytes.len() == 32)
}
fn resolve_programs(root: Option<&Path>, refs: &[String]) -> Result<Vec<String>> {
let lock = root
.map(|root| root.join("arete.lock"))
.filter(|path| path.is_file())
.map(crate::project::lockfile::ProjectLock::load)
.transpose()?;
let mut out: Vec<String> = Vec::new();
for reference in refs {
let ids: Vec<String> = if is_address(reference) {
vec![reference.clone()]
} else {
let Some(dependency) = lock
.as_ref()
.and_then(|lock| lock.dependencies.iter().find(|d| d.alias == *reference))
else {
bail!("'{reference}' is not a program address or an alias in arete.lock");
};
let mut ids: Vec<String> = dependency.program_id.iter().cloned().collect();
ids.extend(dependency.programs.iter().map(|p| p.program_id.clone()));
if ids.is_empty() {
bail!("'{reference}' in arete.lock names no program");
}
ids
};
for id in ids {
if !out.contains(&id) {
out.push(id);
}
}
}
Ok(out)
}
pub fn capture(args: CaptureArgs, json: bool) -> Result<()> {
block_on(async move {
let root = project_root(&args.project).ok();
let programs = resolve_programs(root.as_deref(), &args.programs)?;
if programs.is_empty() {
bail!("name what to capture: --program <address|alias> (repeatable)");
}
let now = chrono::Utc::now().timestamp();
let (from, to) = parse_window(&args.from, args.to.as_deref(), now)?;
let mut options =
RpcCaptureOptions::new(args.rpc_url.clone(), programs, from, to, &args.output);
options.accounts = args.accounts.clone();
options.touched_state = !args.no_touched_state;
options.max_transactions = args.max_transactions;
options.max_signature_pages = args.max_signature_pages;
options.max_state_accounts = args.max_state_accounts;
options.block_order = !args.no_block_order;
options.requests_per_second = args.rps;
options.tool_version = env!("CARGO_PKG_VERSION").to_string();
let rpc = HttpRpc::new(args.rpc_url.clone(), args.rps)?;
let mut progress = |line: &str| {
if !json {
eprintln!("{} {line}", "capture".dimmed());
}
};
let summary = capture_from_rpc(&rpc, &options, &mut progress)
.await
.map_err(|error| anyhow::anyhow!("{error:#}"))?;
let value = serde_json::to_value(&summary)?;
print(json, &value, || print_written(&value));
Ok(())
})
}
fn print_written(result: &Value) {
let header = &result["header"];
println!(
"Wrote {} ({} state accounts, {} updates: {} transactions, {} accounts; slots {}–{}; {})",
result["path"].as_str().unwrap_or("?"),
header["counts"]["state"],
header["counts"]["updates"],
header["counts"]["transactions"],
header["counts"]["accounts"],
header["slotRange"]["first"],
header["slotRange"]["last"],
header["contentHash"].as_str().unwrap_or("?"),
);
if let Some(note) = header["startingState"]["note"].as_str() {
println!(
" starting state: {} ({note})",
header["startingState"]["fidelity"].as_str().unwrap_or("?")
);
}
for limit in header["limits"].as_array().into_iter().flatten() {
println!(" limit: {}", limit.as_str().unwrap_or_default());
}
if let Some(skipped) = result["skippedFailed"].as_u64().filter(|n| *n > 0) {
println!(" skipped {skipped} failed transactions (stacks never see them)");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn program_references_resolve_addresses_without_a_lock() {
let id = "oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv".to_string();
assert_eq!(
resolve_programs(None, &[id.clone(), id.clone()]).unwrap(),
vec![id]
);
assert!(resolve_programs(None, &["ore".into()]).is_err());
}
}