use cu_profiler_core::Result;
use cu_profiler_core::error::Error;
use serde_json::Value;
use crate::args::ImportArgs;
use crate::exit::ExitCode;
pub fn run(args: &ImportArgs, quiet: bool) -> Result<ExitCode> {
let text = std::fs::read_to_string(&args.file)
.map_err(|e| Error::Config(format!("cannot read `{}`: {e}", args.file.display())))?;
let value: Value = serde_json::from_str(&text)
.map_err(|e| Error::Config(format!("`{}` is not valid JSON: {e}", args.file.display())))?;
let logs = find_log_messages(&value).ok_or_else(|| {
Error::Config(format!(
"no `logMessages` array found in `{}` — expected a Solana getTransaction response",
args.file.display()
))
})?;
if logs.is_empty() {
return Err(Error::Config(format!(
"`logMessages` in `{}` was empty — nothing to import",
args.file.display()
)));
}
let name = resolve_name(args)?;
let out = args.logs_dir.join(format!("{name}.log"));
if let Some(parent) = out.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let mut body = logs.join("\n");
body.push('\n');
std::fs::write(&out, body)?;
if !quiet {
println!("imported {} log line(s) -> {}", logs.len(), out.display());
println!(
"next: add `[scenario.{name}]` to your config, then `cu-profiler run --scenario {name}`"
);
}
Ok(ExitCode::Success)
}
fn resolve_name(args: &ImportArgs) -> Result<String> {
if let Some(name) = &args.name {
return Ok(name.clone());
}
args.file
.file_stem()
.and_then(|s| s.to_str())
.map(str::to_string)
.ok_or_else(|| {
Error::Config(format!(
"cannot derive a scenario name from `{}`; pass --name",
args.file.display()
))
})
}
fn find_log_messages(value: &Value) -> Option<Vec<String>> {
match value {
Value::Object(map) => {
if let Some(Value::Array(arr)) = map.get("logMessages") {
let logs: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect();
if !logs.is_empty() {
return Some(logs);
}
}
map.values().find_map(find_log_messages)
}
Value::Array(arr) => arr.iter().find_map(find_log_messages),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn finds_logs_in_rpc_envelope() {
let v: Value = serde_json::from_str(
r#"{"result":{"meta":{"logMessages":["Program P invoke [1]","Program P success"]}}}"#,
)
.unwrap();
let logs = find_log_messages(&v).unwrap();
assert_eq!(logs, vec!["Program P invoke [1]", "Program P success"]);
}
#[test]
fn finds_logs_when_nested_in_array() {
let v: Value =
serde_json::from_str(r#"[{"meta":{"logMessages":["Program X success"]}}]"#).unwrap();
assert_eq!(find_log_messages(&v).unwrap(), vec!["Program X success"]);
}
#[test]
fn none_when_absent() {
let v: Value = serde_json::from_str(r#"{"meta":{"err":null}}"#).unwrap();
assert!(find_log_messages(&v).is_none());
}
#[test]
fn name_defaults_to_file_stem() {
let args = ImportArgs {
file: Path::new("/tmp/okx_swap.json").to_path_buf(),
name: None,
logs_dir: Path::new(".cu/logs").to_path_buf(),
};
assert_eq!(resolve_name(&args).unwrap(), "okx_swap");
}
}