use clap::{App, Arg, SubCommand};
use ethereum_tx_sign::RawTransaction;
use ethers::prelude::*;
use ethers::providers::{Http, Provider};
use ethers::signers::LocalWallet;
use ethers::utils::{hex, keccak256, parse_ether};
use fs_extra::dir::CopyOptions;
use parser::{append_js_functions_to_file, generate_import_statement, read_first_account};
use reqwest::Error as ReqwestError;
use serde::{Deserialize, Serialize};
use std::env::current_dir;
use std::error::Error;
use std::fs::{self, OpenOptions};
use std::io::{stderr, stdin, stdout, Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use std::{collections::HashMap, os::unix::io, sync::Arc};
use std::{env, primitive};
use tokio::sync::Mutex;
use warp::filters::body::form;
use web3::Web3;
mod confighandler;
mod filehandler;
mod parser;
use crate::confighandler::{configgenerator, Config};
use crate::filehandler::{build_index_content, index_content};
use crate::filehandler::{filewriter, state_content, tsconfig_content, utils_content};
use crate::parser::configparser;
use crate::parser::create_js_function;
use crate::parser::importparser;
use crate::parser::parse_typescript_file;
use crate::parser::remove_unwanted_lines;
#[derive(Debug, Deserialize)]
struct TokenResponse {
access_token: String,
expires_in: u64,
token_type: String,
refresh_token: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct SignedBinary {
byteCode: Vec<u8>,
signature: String,
appID: i32,
}
#[derive(Serialize, Deserialize)]
struct Genesis {
signedBinary: SignedBinary,
genesisState: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let matches = App::new("My Rust CLI")
.version("1.0")
.author("Your Name")
.about("A CLI with Google Auth")
.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),
)
.arg(
Arg::with_name("private-key")
.long("private-key")
.help("Takes private key of the operator")
.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(),
batch_size: 0,
batch_time: 0,
batcher_slot_time: 0,
operator_accounts: Vec::new(),
aggregator_rpcs: Vec::new(),
};
let client_id = "266578720767-7qf52i2g90hjjlv258b2bra1ktt4f9lv.apps.googleusercontent.com";
let clientt_secret = "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";
println!(
r#"
_ _ _ _
___| |_ __ _ ___| | ___ __ ___| (_)
/ __| __/ _` |/ __| |/ / '__| _____ / __| | |
\__ \ || (_| | (__| <| | |_____| | (__| | |
|___/\__\__,_|\___|_|\_\_| \___|_|_|
"#
);
println!("Enter into the world of Micro Rollups");
if let Some(project_name) = init_matches.value_of("project-name") {
config.project_name = project_name.trim().to_string();
}
if let Some(private_key) = init_matches.value_of("private-key") {
let mut operator_address = private_key.trim();
operator_address = operator_address.trim_start_matches("0x");
match operator_address.len() == 64 {
true => println!(""),
false => {
println!("Invalid private key length");
std::process::exit(1);
}
}
config.operator_accounts.push(operator_address.to_string());
}
config.batch_time = 3000;
config.batch_time = 10;
config.batch_time = 1000;
println!("Creating project ... {}", &config.project_name);
let _ = fs::create_dir(&config.project_name);
let mut clone_command = Command::new("git");
clone_command.arg("clone")
.arg("https://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@github.com/stackrlabs/js-sdk-examples.git")
.current_dir(&config.project_name);
match clone_command.status() {
Ok(status) => {
if status.success() {
println!("Repository cloned successfully.");
} else {
eprintln!("Failed to clone repository: {:?}", status);
}
}
Err(err) => {
eprintln!("Failed to execute git clone: {:?}", err);
}
}
let _ = setup_project(&config.project_name);
let bun_install = Command::new("bun").arg("install").current_dir(&config.project_name);
}
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 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(31337u64);
let provider = Provider::<Http>::try_from("http://127.0.0.1:8545/")?;
let client = SignerMiddleware::new(provider, wallet.clone());
let contract_address = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
let short_hash = "abc";
let padded_hash = format!("{:0<64}", short_hash);
let state_machine_hash = H256::from_str(&padded_hash)?;
let genesis_state_hash = H256::from_str(&padded_hash)?;
println!("{:?}", state_machine_hash);
println!("{:?}", genesis_state_hash);
let function_signature = "registerApp(bytes32,bytes32)";
let function_selector = &keccak256(function_signature)[0..4];
println!("{:?}", function_selector);
let mut data = Vec::new();
data.extend_from_slice(function_selector);
data.extend_from_slice(&state_machine_hash.0);
data.extend_from_slice(&genesis_state_hash.0);
let tx = TransactionRequest::new()
.to(contract_address.parse::<Address>()?)
.value(parse_ether("0")?)
.data(data);
println!("{:?}", tx);
let tx_hash = client.send_transaction(tx, None).await?.await?;
println!("Transaction Hash: {:?}", tx_hash);
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!("Transpiling stf.ts from TS -> 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 exchange_code_for_token(
authorization_code: String,
client_id: String,
client_secret: String,
redirect_uri: &str,
) -> Result<TokenResponse, ReqwestError> {
let params = [
("code", authorization_code),
("client_id", client_id),
("client_secret", client_secret),
("redirect_uri", redirect_uri.to_string()),
("grant_type", "authorization_code".to_string()),
];
let client = reqwest::Client::new();
let res = client
.post("https://oauth2.googleapis.com/token")
.form(¶ms)
.send()
.await?;
let token_response: TokenResponse = res.json().await?;
Ok(token_response)
}
fn read_u32(prompt: &str, default: u32) -> u32 {
loop {
println!("{} (default {}):", prompt, default);
let mut input = String::new();
if stdin().read_line(&mut input).is_err() {
println!("Error reading input. Using default value.");
return default;
}
let input = input.trim();
if input.is_empty() {
return default;
}
match input.parse::<u32>() {
Ok(num) => return num,
Err(_) => println!("Invalid input. Please enter a valid number or just press enter to use the default."),
}
}
}
async fn deploy_command() -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
let current_dir = env::current_dir()?;
let first_account_pvt_key = read_first_account(¤t_dir.join("stackr.config.ts"))?;
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 wallet = first_account_pvt_key.parse::<LocalWallet>()?;
println!("Using the account : {}", wallet.address());
let byte_code_signature =
ethers::signers::Wallet::sign_hash(&wallet, (&byte_code_hash).into())?;
let signed_binary = SignedBinary {
byteCode: byte_code.clone(),
signature: byte_code_signature.to_string(),
appID: 0,
};
let genesis_state = "0".to_string();
let payload = Genesis {
signedBinary: signed_binary,
genesisState: genesis_state,
};
let payload_json = serde_json::to_string(&payload)?;
let res = client
.post("http://localhost:3000/v0/genesis")
.header("Content-Type", "application/json")
.body(payload_json)
.send()
.await?;
if res.status().is_success() {
println!("Deployed successfully.");
} else {
eprintln!(
"Failed to deploy: {:?} {:?}",
res.status(),
res.error_for_status()
);
}
Ok(())
}
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 delete_directory(dir_path: &str) -> Result<(), std::io::Error> {
fs::remove_dir_all(dir_path)
}
fn setup_project(project_name: &str) -> Result<(), Box<dyn Error>> {
let source_dir = format!("{}/js-sdk-examples/counter", &project_name); let dest_dir = &project_name;
if !fs::metadata(dest_dir).is_ok() {
fs::create_dir(dest_dir)?;
}
let mut options = CopyOptions::new();
options.overwrite = true;
options.content_only = true;
if let Err(err) = fs_extra::dir::copy(source_dir, dest_dir, &options) {
eprintln!("Error copying directory: {:?}", err);
}
if let Err(err) = delete_directory("proj/js-sdk-examples") {
eprintln!("Error deleting directory: {:?}", err);
} else {
println!(
"Template generated successfully"
);
}
Ok(())
}