stackr 0.1.10

CLI to initialize, develop, and maintain your Stackr Micro-rollups
use regex::Regex;
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;

#[derive(Debug)]
pub struct ParsedData {
    rollup_name: String,
    stf_function: String,
}

pub fn importparser(destination_file_path: &PathBuf) -> io::Result<()> {
    let current_dir = env::current_dir()?;

    let content = fs::read_to_string(&destination_file_path)?;

    let re_usage =
        Regex::new(r"\bethers(?:\.([a-zA-Z0-9_]+))?\.[a-zA-Z0-9_]+\b").unwrap();

    let mut ethers_members = HashMap::new();

    let new_content =
        re_usage.replace_all(&content, |caps: &regex::Captures| {
            let member = caps[0].to_string();
            let split: Vec<&str> = member.split('.').collect();
            let (import_path, import_member) = match split.as_slice() {
                [_, subpath, name] => {
                    (format!("ethers/{}", subpath), name.to_string())
                }
                [_, name] => ("ethers".to_string(), name.to_string()),
                _ => unreachable!(),
            };

            ethers_members
                .entry(import_member.clone())
                .or_insert_with(|| import_path);

            import_member
        });

    let mut import_lines = String::new();
    for (member, path) in &ethers_members {
        import_lines
            .push_str(&format!("import {{{}}} from \"{}\";\n", member, path));
    }
    if !import_lines.is_empty() {
        import_lines.push('\n');
    }

    let final_content = format!("{}{}", import_lines, new_content);

    let mut output_file = fs::File::create(&destination_file_path)?;
    output_file.write_all(final_content.as_bytes())?;

    Ok(())
}

pub fn configparser() -> io::Result<()> {
    let current_dir = env::current_dir()?;

    let destination_file_path = current_dir.join(format!("stackr.config.ts"));

    let content = fs::read_to_string(&destination_file_path)?;

    // Parse the JSON content into a serde_json::Value
    let json_value: Value = serde_json::from_str(&content)?;

    // Convert the JSON value to a string with unquoted keys
    let js_object_literal = format!(
        "const stackrConfig: StackrConfig = {};",
        json_to_js_object_literal(&json_value)
    );

    // Write the JS object literal to a new file
    let mut output_file = fs::File::create(&destination_file_path)?;
    output_file.write_all(js_object_literal.as_bytes())?;

    Ok(())
}

pub fn remove_unwanted_lines(file_path: &PathBuf) -> io::Result<()> {
    println!("Sanitizing files ...");

    let data = fs::read_to_string(&file_path)?;
    let console_log_regex = Regex::new(r"^\s*console\.log\(.*\);\s*$")
        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
    let import_ethers_regex =
        Regex::new(r#"import\s+\{\s*ethers\s*\}\s+from\s+['"]ethers['"];"#)
            .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
    let cleaned_data = data
        .lines()
        .filter(|line| {
            (!console_log_regex.is_match(line)
                || !import_ethers_regex.is_match(line))
        })
        .collect::<Vec<&str>>()
        .join("\n");

    let mut file = fs::File::create(&file_path)?;
    file.write_all(cleaned_data.as_bytes())?;

    println!("Sanitization complete");
    Ok(())
}

pub fn append_js_functions_to_file<P: AsRef<Path>>(
    file_path: P,
) -> std::io::Result<()> {
    let js_code = r#"

    // Execution step
// Read inputs from stdin
const stfInputs = readInput();
var newState = stfInputs.currentState;
//this sets all product descriptions to a max length of 10 characters
stfInputs.actions.forEach((action) => {
  newState = counterSTF.apply(action, newState);
});
// Write the result to stdout
writeOutput(newState);

function readInput() {
    const chunkSize = 1024;
    const inputChunks = [];
    let totalBytes = 0;

    // Read all the available bytes
    while (1) {
        const buffer = new Uint8Array(chunkSize);
        // Stdin file descriptor
        const fd = 0;
        const bytesRead = Javy.IO.readSync(fd, buffer);

        totalBytes += bytesRead;
        if (bytesRead === 0) {
            break;
        }
        inputChunks.push(buffer.subarray(0, bytesRead));
    }

    // Assemble input into a single Uint8Array
    const { finalBuffer } = inputChunks.reduce((context, chunk) => {
        context.finalBuffer.set(chunk, context.bufferOffset);
        context.bufferOffset += chunk.length;
        return context;
    }, { bufferOffset: 0, finalBuffer: new Uint8Array(totalBytes) });

    return JSON.parse(new TextDecoder().decode(finalBuffer));
}

// Write output to stdout
function writeOutput(output) {
    const encodedOutput = new TextEncoder().encode(JSON.stringify(output));
    const buffer = new Uint8Array(encodedOutput);
    // Stdout file descriptor
    const fd = 1;
    Javy.IO.writeSync(fd, buffer);
}
"#;

    let current_dir = env::current_dir()?;

    let _ = Command::new("mkdir")
        .arg("build")
        .current_dir(&current_dir)
        .output();

    let build_file_path = current_dir.join("build/state.ts");

    let _ = std::fs::copy(&file_path, &build_file_path);

    let mut file = OpenOptions::new()
        .write(true)
        .append(true)
        .open(&build_file_path)?;

    writeln!(file, "{}", js_code)?;

    Ok(())
}

// Function to convert JSON Value to a JavaScript object literal string
fn json_to_js_object_literal(value: &Value) -> String {
    match value {
        Value::Object(map) => map_to_js_object_literal(map),
        _ => value.to_string(),
    }
}

// Helper function to convert a serde_json Map to a JS object literal string
fn map_to_js_object_literal(map: &Map<String, Value>) -> String {
    let mut object_literal = String::from("{");

    for (i, (k, v)) in map.iter().enumerate() {
        if i > 0 {
            object_literal.push_str(", ");
        }
        object_literal.push_str(&format!(
            "{}: {}",
            k,
            json_to_js_object_literal(v)
        ));
    }

    object_literal.push('}');
    object_literal
}

pub fn read_first_account<P: AsRef<Path>>(
    file_path: P,
) -> Result<String, Box<dyn Error>> {
    let data = fs::read_to_string(&file_path)?;

    // Regular expression to match the accounts array
    let re = Regex::new(r#"privateKey:\s*\"([^\"]+)\""#)?;

    if let Some(caps) = re.captures(&data) {
        if let Some(account) = caps.get(1) {
            return Ok(account.as_str().to_string());
        }
    }

    Err("No account found".into())
}

pub fn read_vulcan_rpc<P: AsRef<Path>>(
    file_path: P,
) -> Result<String, Box<dyn Error>> {
    let data = fs::read_to_string(&file_path)?;

    // Regular expression to match the accounts array
    let re = Regex::new(r#"vulcanRPC:\s*\"([^"]+)\""#)?;

    if let Some(caps) = re.captures(&data) {
        if let Some(account) = caps.get(1) {
            return Ok(account.as_str().to_string());
        }
    }

    Err("No vulcan RPC found".into())
}

pub fn read_l1_rpc<P: AsRef<Path>>(
    file_path: P,
) -> Result<String, Box<dyn Error>> {
    let data = fs::read_to_string(&file_path)?;

    // Regular expression to match the accounts array
    let re = Regex::new(r#"L1RPC:\s*\"([^"]+)\""#)?;

    if let Some(caps) = re.captures(&data) {
        if let Some(account) = caps.get(1) {
            return Ok(account.as_str().to_string());
        }
    }

    Err("No L1 RPC found".into())
}

pub fn generate_import_statement(
    file_path: &PathBuf,
) -> Result<String, Box<dyn Error>> {
    let content = fs::read_to_string(file_path)?;

    let re =
        Regex::new(r"export (?:type|class) (\w+)|export const (\w+)").unwrap();

    let mut exports = vec![];

    for caps in re.captures_iter(&content) {
        if let Some(export_name) = caps.get(1).or_else(|| caps.get(2)) {
            exports.push(export_name.as_str().to_string());
        }
    }

    let import_statement = if !exports.is_empty() {
        format!("import {{ {} }} from \"./state\";", exports.join(", "))
    } else {
        String::new()
    };

    Ok(import_statement)
}

pub fn create_js_function(
    import_statement: &str,
) -> Result<String, Box<dyn Error>> {
    let current_dir = env::current_dir()?;
    let state_file_dir = current_dir.join("src/state.ts");

    let result = parse_typescript_file(&state_file_dir)?;

    let rollup_name = &result.rollup_name;
    let stf_name = &result.stf_function;

    let first_char = rollup_name
        .chars()
        .next()
        .unwrap()
        .to_lowercase()
        .to_string();
    let lowercase_rollup_name = first_char + &rollup_name[1..] + "Object";

    Ok(format!(
        r#"const run = (prevState: StateVariable, actions: any[]) => {{
    const {} = new {}(prevState);
    const fsm = new StateMachine({{
      state: {},
      stf: {},
    }});

    actions.forEach((action: any) => {{
      fsm.apply(action);
    }});

    return fsm.state.getState();
}};"#,
        lowercase_rollup_name, rollup_name, lowercase_rollup_name, stf_name
    ))
}

pub fn parse_typescript_file(
    file_path: &PathBuf,
) -> Result<ParsedData, Box<dyn Error>> {
    let content = fs::read_to_string(file_path)?;

    let rollup_regex = Regex::new(r"export class (\w+) extends RollupState<")?;
    let rollup_name = rollup_regex
        .captures(&content)
        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
        .ok_or("RollupState class not found")?;

    let stf_regex = Regex::new(r"export const (\w+): STF<")?;
    let stf_function = stf_regex
        .captures(&content)
        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
        .ok_or("STF function not found")?;

    Ok(ParsedData {
        rollup_name,
        stf_function,
    })
}