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(¤t_dir.join("stackr.config.ts"))?;
println!("{}", rpc);
let provider = Provider::<Http>::try_from(rpc)?;
let first_account_pvt_key =
read_first_account(¤t_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, ¤t_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(¤t_dir.join("build/state.ts"));
let _ = remove_unwanted_lines(¤t_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(¤t_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!("{}", ¤t_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(¤t_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()?;
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);
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(),
);
let combined_hash = [&byte_code_hash[..], &genesis_state_hash[..]].concat();
let combined_hash = ethers::utils::keccak256(&combined_hash);
let rpc = read_l1_rpc(¤t_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(¤t_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?;
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?;
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(¤t_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>> {
let mut file = File::open(file_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
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>> {
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(())
}