stackr 0.1.10

CLI to initialize, develop, and maintain your Stackr Micro-rollups
use clap::{App, Arg, SubCommand};
use ethers::prelude::*;
use ethers::providers::{Http, Provider};
use ethers::signers::LocalWallet;
use parser::{generate_import_statement, read_first_account, read_l1_rpc};
use reqwest::multipart;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::env;
use std::error::Error;
use std::fs::{self, File, OpenOptions};
use std::io::{stderr, stdout, Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use std::sync::Arc;

mod confighandler;
mod constants;
mod filehandler;
mod parser;

use crate::confighandler::Config;
use crate::constants::REGISTRY_CONTRACT;
use crate::filehandler::build_index_content;
use crate::filehandler::setup_project;
use crate::parser::importparser;
use crate::parser::remove_unwanted_lines;
use crate::parser::{create_js_function, read_vulcan_rpc};

#[derive(Serialize, Deserialize)]
struct Genesis {
    appId: u64,
    genesisState: Value,
}

#[derive(Serialize, Deserialize)]
struct DeploymentObject {
    app_id: u64,
    app_inbox: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let matches = App::new("Stackr CLI for Micro-rollups")
        .subcommand(
            SubCommand::with_name("init")
                .about("Initialize the application")
                .arg(
                    Arg::with_name("project-name")
                        .long("project-name")
                        .help("Sets the project name")
                        .takes_value(true)
                        .required(true),
                ),
        )
        .subcommand(SubCommand::with_name("compile"))
        .subcommand(SubCommand::with_name("deploy"))
        .subcommand(SubCommand::with_name("register"))
        .get_matches();

    if let Some(init_matches) = matches.subcommand_matches("init") {
        let mut config = Config {
            project_name: String::new(),
        };
        if let Some(project_name) = init_matches.value_of("project-name") {
            config.project_name = project_name.trim().to_string();
        }
        let _ = setup_project(&config.project_name).await?;
    }

    if matches.subcommand_matches("compile").is_some() {
        println!("Compiling...");
        let _ = compilation();
    }

    if matches.subcommand_matches("deploy").is_some() {
        println!("Deploying...");
        deploy_command().await?;
    }

    if matches.subcommand_matches("register").is_some() {
        println!("Registering...");
        register().await?;
    }

    Ok(())
}

async fn register() -> Result<(), Box<dyn Error>> {
    let current_dir = env::current_dir()?;

    let rpc = read_l1_rpc(&current_dir.join("stackr.config.ts"))?;
    println!("{}", rpc);
    let provider = Provider::<Http>::try_from(rpc)?;

    let first_account_pvt_key =
        read_first_account(&current_dir.join("stackr.config.ts"))?;

    let chain_id = provider.get_chainid().await?;
    let wallet: LocalWallet = first_account_pvt_key
        .parse::<LocalWallet>()?
        .with_chain_id(chain_id.as_u64());

    println!("Using the account : {}", wallet.address());
    println!("Using the chain id : {}", chain_id);

    abigen!(
        Registry,
        "./contracts/registry.json",
        event_derives(serde::Deserialize, serde::Serialize)
    );

    let client = SignerMiddleware::new(provider, wallet.clone());

    let contract_address = REGISTRY_CONTRACT;
    let h160_contract_address = match H160::from_str(contract_address) {
        Ok(address) => address,
        Err(e) => return Err(Box::new(e)),
    };
    let registry =
        Registry::new(h160_contract_address, Arc::new(client.clone()));

    let receipt: TransactionReceipt =
        registry.register_app().send().await?.await?.unwrap();

    let _ = save_deployment(receipt.logs[0].clone());

    Ok(())
}

fn compilation() -> Result<(), Box<dyn Error>> {
    let current_dir = env::current_dir()?;
    let build_dir = current_dir.join("build");
    if !build_dir.exists() {
        fs::create_dir(&build_dir)?;
        println!("🤓 Created build directory: {:?}", build_dir);
    }
    let state_file_dir = current_dir.join("src/state.ts");
    let _ = fs::copy(&state_file_dir, &current_dir.join("build/state.ts"));

    let import_statement = generate_import_statement(&state_file_dir)?;

    let run_function = create_js_function(&import_statement);

    let _ = fs::write(current_dir.join("build/stf.ts"), import_statement);

    let mut build_index = OpenOptions::new()
        .append(true)
        .open(current_dir.join("build/stf.ts"))?;
    let _ = build_index.write("\n".as_bytes());
    let _ = build_index.write(
        r#"import { StateMachine } from "@stackr/stackr-js/execution";"#
            .as_bytes(),
    );
    let _ = build_index.write("\n\n".as_bytes());
    let _ = build_index.write(run_function?.as_bytes());
    let _ = build_index.write("\n\n".as_bytes());
    let _ = build_index.write(build_index_content().as_bytes());

    let _ = importparser(&current_dir.join("build/state.ts"));
    let _ = remove_unwanted_lines(&current_dir.join("build/state.ts"));

    println!("🔁 Extracting Rollup logic from state -> JS");

    let js_transpile = Command::new("bun")
        .args(&[
            "build",
            "./build/stf.ts",
            "--outdir=build",
            "--target=bun",
            "--external=class-transformer",
            "--external=class-validator",
            "--external=@nestjs/microservices",
            "--external=@nestjs/websockets",
            "--external=@nestjs/platform-express",
        ])
        .current_dir(&current_dir)
        .output();

    match js_transpile {
        Ok(output) => {
            stdout().write_all(&output.stdout)?;
            stderr().write_all(&output.stderr)?;
            if !output.status.success() {
                eprintln!("js -> wasm failed: {}", output.status);
                std::process::exit(1);
            }
        }
        Err(e) => {
            eprintln!("Failed to transpile: {}", e);
            eprintln!("{}", &current_dir.display());
            std::process::exit(1);
        }
    }

    println!("☢️ Creating a WASM build");

    let javy_compile = Command::new("javy")
        .arg("compile")
        .arg("./build/stf.js")
        .arg("-o")
        .arg("./build/stf.wasm")
        .current_dir(&current_dir)
        .output();

    match javy_compile {
        Ok(output) => {
            stdout().write_all(&output.stdout)?;
            stderr().write_all(&output.stderr)?;
            if !output.status.success() {
                eprintln!("js -> wasm failed: {}", output.status);
                std::process::exit(1);
            }
        }
        Err(e) => {
            eprintln!("Failed to execute javy compile: {}", e);
            std::process::exit(1);
        }
    }

    println!("✅ Compilation done!");
    Ok(())
}

async fn deploy_command() -> Result<(), Box<dyn Error>> {
    let current_dir = env::current_dir()?;

    // Binary
    let binary_path = current_dir.join("build/stf.wasm");
    let byte_code = read_byte_code(&binary_path).await?;
    let byte_code_hash: [u8; 32] = ethers::utils::keccak256(&byte_code);

    // Genesis State
    let genesis_state_path = current_dir.join("genesis-state.json");
    let genesis_state = load_json_file(&genesis_state_path).unwrap();
    let genesis_state_hash: [u8; 32] = ethers::utils::keccak256(
        &genesis_state.get("state").unwrap().to_string().as_bytes(),
    );

    // Combine Hashes
    let combined_hash = [&byte_code_hash[..], &genesis_state_hash[..]].concat();
    let combined_hash = ethers::utils::keccak256(&combined_hash);

    let rpc = read_l1_rpc(&current_dir.join("stackr.config.ts"))?;
    let provider = Provider::<Http>::try_from(rpc)?;
    let chain_id = provider.get_chainid().await?;

    let first_account_pvt_key =
        read_first_account(&current_dir.join("stackr.config.ts"))?;

    let wallet: LocalWallet = first_account_pvt_key
        .parse::<LocalWallet>()?
        .with_chain_id(chain_id.as_u64());

    let signature = wallet.sign_message(&combined_hash).await?;

    // Prepare for AppInbox

    let deployment_json_path = current_dir.join("deployment.json");
    let app_inbox_json = load_json_file(&deployment_json_path).unwrap();
    let app_inbox_data =
        app_inbox_json.get("app_inbox").unwrap().as_str().unwrap();
    let app_id_data = app_inbox_json.get("app_id").unwrap().as_u64().unwrap();

    let app_inbox_address = match H160::from_str(app_inbox_data) {
        Ok(address) => address,
        Err(e) => return Err(Box::new(e)),
    };

    abigen!(
        AppInbox,
        "./contracts/app-inbox.json",
        event_derives(serde::Deserialize, serde::Serialize)
    );

    let client2 = SignerMiddleware::new(provider, wallet.clone());

    let app_inbox = AppInbox::new(app_inbox_address, Arc::new(client2.clone()));

    app_inbox
        .register_stf(
            AppData {
                state_machine_hash: byte_code_hash,
                genesis_state_hash: genesis_state_hash,
            },
            signature.to_vec().into(),
        )
        .send()
        .await?;

    // Prepare for Vulcan

    let payload = Genesis {
        appId: app_id_data,
        genesisState: genesis_state,
    };

    let payload_json = serde_json::to_string(&payload)?;

    std::thread::sleep(std::time::Duration::from_secs(5));

    let vulcan_rpc_base =
        read_vulcan_rpc(&current_dir.join("stackr.config.ts"))?;
    println!("{}", vulcan_rpc_base);
    let vulcan_rpc = format!("{}/v0/genesis", vulcan_rpc_base);

    let client = reqwest::Client::new();

    let file_content = fs::read(&binary_path)?;

    let form = multipart::Form::new()
        .part(
            "binary",
            multipart::Part::bytes(file_content).file_name("stf.wasm"),
        )
        .text("payload", payload_json);

    let res = client.post(vulcan_rpc).multipart(form).send().await?;

    if res.status().is_success() {
        println!("Deployed successfully.");
    } else {
        eprintln!(
            "Failed to deploy: {:?} {:?}",
            res.status(),
            res.error_for_status()
        );
    }

    Ok(())
}

fn load_json_file(
    file_path: &Path,
) -> Result<Value, Box<dyn std::error::Error>> {
    // Open the file
    let mut file = File::open(file_path)?;

    // Read the file content into a String
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

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

    Ok(json_value)
}

async fn read_byte_code(
    file_path: &PathBuf,
) -> Result<Vec<u8>, Box<dyn Error>> {
    // Asynchronously read the contents of the file.
    let mut file = fs::File::open(file_path)?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;
    Ok(buffer)
}

fn save_deployment(log: Log) -> Result<(), Box<dyn Error>> {
    let decoded = abi::decode(
        &[
            abi::ParamType::Uint(256),
            abi::ParamType::Address,
            abi::ParamType::Address,
        ],
        &log.data,
    )
    .unwrap();

    let deployment_object = DeploymentObject {
        app_id: u64::from_str_radix(&decoded[0].to_string(), 16).unwrap(),
        app_inbox: format!("0x{}", decoded[2].to_string()),
    };

    let json_string = serde_json::to_string(&deployment_object)?;

    let current_dir = env::current_dir()?;
    let deployment_json_path = current_dir.join("deployment.json");

    fs::write(deployment_json_path, json_string.clone())?;

    println!("⤵️ Deployment saved to deployment.json {}", json_string);
    Ok(())
}