From e528ace725fbb341ae064dfa10e97fbc02754f46 Mon Sep 17 00:00:00 2001 From: hu55a1n1 Date: Fri, 19 Jul 2024 14:19:43 +0200 Subject: [PATCH 1/7] Setup skeleton for quartz tool --- Cargo.lock | 13 +++++++++++ Cargo.toml | 1 + cli/Cargo.toml | 19 ++++++++++++++++ cli/README.md | 1 + cli/src/cli.rs | 42 +++++++++++++++++++++++++++++++++++ cli/src/error.rs | 8 +++++++ cli/src/handler.rs | 22 ++++++++++++++++++ cli/src/handler/init.rs | 16 ++++++++++++++ cli/src/main.rs | 48 ++++++++++++++++++++++++++++++++++++++++ cli/src/request.rs | 19 ++++++++++++++++ cli/src/request/init.rs | 30 +++++++++++++++++++++++++ cli/src/response.rs | 10 +++++++++ cli/src/response/init.rs | 12 ++++++++++ 13 files changed, 241 insertions(+) create mode 100644 cli/Cargo.toml create mode 100644 cli/README.md create mode 100644 cli/src/cli.rs create mode 100644 cli/src/error.rs create mode 100644 cli/src/handler.rs create mode 100644 cli/src/handler/init.rs create mode 100644 cli/src/main.rs create mode 100644 cli/src/request.rs create mode 100644 cli/src/request/init.rs create mode 100644 cli/src/response.rs create mode 100644 cli/src/response/init.rs diff --git a/Cargo.lock b/Cargo.lock index d5811c34..0e72894f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2332,6 +2332,19 @@ dependencies = [ "prost", ] +[[package]] +name = "quartz" +version = "0.1.0" +dependencies = [ + "clap", + "color-eyre", + "displaydoc", + "serde", + "thiserror", + "tracing", + "tracing-subscriber", +] + [[package]] name = "quartz-app-mtcs-enclave" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8e0d2b6b..d872644c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "apps/mtcs/enclave", "apps/transfers/enclave", + "cli", "core/light-client-proofs/*", "core/quartz", "cosmwasm/packages/*", diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 00000000..75038ddf --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "quartz" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +keywords = ["blockchain", "cosmos", "tendermint", "cycles", "quartz"] +readme = "README.md" + +[dependencies] +clap.workspace = true +color-eyre.workspace = true +displaydoc.workspace = true +serde.workspace = true +thiserror.workspace = true +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..61fb3486 --- /dev/null +++ b/cli/README.md @@ -0,0 +1 @@ +# quartz CLI diff --git a/cli/src/cli.rs b/cli/src/cli.rs new file mode 100644 index 00000000..285568ab --- /dev/null +++ b/cli/src/cli.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; +use tracing::metadata::LevelFilter; + +#[derive(clap::Args, Debug, Clone)] +pub struct Verbosity { + /// Increase verbosity, can be repeated up to 2 times + #[arg(long, short, action = clap::ArgAction::Count)] + pub verbose: u8, +} + +impl Verbosity { + pub fn to_level_filter(&self) -> LevelFilter { + match self.verbose { + 0 => LevelFilter::INFO, + 1 => LevelFilter::DEBUG, + _ => LevelFilter::TRACE, + } + } +} + +#[derive(Debug, Parser)] +#[command(version, long_about = None)] +pub struct Cli { + /// Increase log verbosity + #[clap(flatten)] + pub verbose: Verbosity, + + /// Main command + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + Init { + /// path to create & init a quartz app, defaults to current path if unspecified + #[clap(long)] + path: Option, + }, +} diff --git a/cli/src/error.rs b/cli/src/error.rs new file mode 100644 index 00000000..18b2efe8 --- /dev/null +++ b/cli/src/error.rs @@ -0,0 +1,8 @@ +use displaydoc::Display; +use thiserror::Error; + +#[derive(Debug, Display, Error)] +pub enum Error { + /// specified path `{0}` is not a directory + PathNotDir(String), +} diff --git a/cli/src/handler.rs b/cli/src/handler.rs new file mode 100644 index 00000000..8377c68c --- /dev/null +++ b/cli/src/handler.rs @@ -0,0 +1,22 @@ +use crate::{cli::Verbosity, error::Error, request::Request, response::Response}; + +pub mod init; + +pub trait Handler { + type Error; + type Response; + + fn handle(self, verbosity: Verbosity) -> Result; +} + +impl Handler for Request { + type Error = Error; + type Response = Response; + + fn handle(self, verbosity: Verbosity) -> Result { + match self { + Request::Init(request) => request.handle(verbosity), + } + .map(Into::into) + } +} diff --git a/cli/src/handler/init.rs b/cli/src/handler/init.rs new file mode 100644 index 00000000..e7f251f4 --- /dev/null +++ b/cli/src/handler/init.rs @@ -0,0 +1,16 @@ +use tracing::trace; + +use crate::{ + cli::Verbosity, error::Error, handler::Handler, request::init::InitRequest, + response::init::InitResponse, +}; + +impl Handler for InitRequest { + type Error = Error; + type Response = InitResponse; + + fn handle(self, _verbosity: Verbosity) -> Result { + trace!("initializing directory structure..."); + todo!() + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 00000000..b54cce99 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,48 @@ +#![doc = include_str!("../README.md")] +#![forbid(unsafe_code)] +#![warn( + clippy::checked_conversions, + clippy::panic, + clippy::panic_in_result_fn, + clippy::unwrap_used, + trivial_casts, + trivial_numeric_casts, + rust_2018_idioms, + unused_lifetimes, + unused_import_braces, + unused_qualifications +)] + +pub mod cli; +pub mod error; +pub mod handler; +pub mod request; +pub mod response; + +use clap::Parser; +use color_eyre::eyre::Result; +use tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; + +use crate::{cli::Cli, handler::Handler, request::Request}; + +fn main() -> Result<()> { + color_eyre::install()?; + + let args = Cli::parse(); + + let env_filter = EnvFilter::builder() + .with_default_directive(args.verbose.to_level_filter().into()) + .from_env_lossy(); + + tracing_subscriber::fmt() + .with_target(false) + .with_writer(std::io::stderr) + .with_env_filter(env_filter) + .finish() + .init(); + + let request = Request::try_from(args.command)?; + request.handle(args.verbose)?; + + Ok(()) +} diff --git a/cli/src/request.rs b/cli/src/request.rs new file mode 100644 index 00000000..07b90a7f --- /dev/null +++ b/cli/src/request.rs @@ -0,0 +1,19 @@ +use crate::{cli::Command, error::Error, request::init::InitRequest}; + +pub mod init; + +#[derive(Clone, Debug)] +pub enum Request { + Init(InitRequest), +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(cmd: Command) -> Result { + match cmd { + Command::Init { path } => InitRequest::try_from(path), + } + .map(Into::into) + } +} diff --git a/cli/src/request/init.rs b/cli/src/request/init.rs new file mode 100644 index 00000000..3d93c0d7 --- /dev/null +++ b/cli/src/request/init.rs @@ -0,0 +1,30 @@ +use std::path::PathBuf; + +use crate::{error::Error, request::Request}; + +#[derive(Clone, Debug)] +pub struct InitRequest { + // TODO(hu55a1n1): remove `allow(unused)` here once init handler is implemented + #[allow(unused)] + directory: PathBuf, +} + +impl TryFrom> for InitRequest { + type Error = Error; + + fn try_from(path: Option) -> Result { + if let Some(path) = path { + if !path.is_dir() { + return Err(Error::PathNotDir(format!("{}", path.display()))); + } + } + + todo!() + } +} + +impl From for Request { + fn from(request: InitRequest) -> Self { + Self::Init(request) + } +} diff --git a/cli/src/response.rs b/cli/src/response.rs new file mode 100644 index 00000000..5e53a5d6 --- /dev/null +++ b/cli/src/response.rs @@ -0,0 +1,10 @@ +use serde::Serialize; + +use crate::response::init::InitResponse; + +pub mod init; + +#[derive(Clone, Debug, Serialize)] +pub enum Response { + Init(InitResponse), +} diff --git a/cli/src/response/init.rs b/cli/src/response/init.rs new file mode 100644 index 00000000..8f5a2002 --- /dev/null +++ b/cli/src/response/init.rs @@ -0,0 +1,12 @@ +use serde::Serialize; + +use crate::response::Response; + +#[derive(Clone, Debug, Serialize)] +pub struct InitResponse; + +impl From for Response { + fn from(response: InitResponse) -> Self { + Self::Init(response) + } +} From 9bc55fcb106ddda7921d9c40c4b8756b8776b0be Mon Sep 17 00:00:00 2001 From: hu55a1n1 Date: Mon, 22 Jul 2024 13:12:20 +0200 Subject: [PATCH 2/7] Update README.md for the CLI --- cli/README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/cli/README.md b/cli/README.md index 61fb3486..367aeb4d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1 +1,77 @@ # quartz CLI + +A CLI tool to manage Quartz applications. The `quartz` CLI tool is designed to streamline the development and deployment +process of Quartz applications. + +It provides helpful information about each command and its options. To get a list of all available subcommands and their +descriptions, use the `--help` flag: + +```shell +$ quartz --help + +Quartz 0.1.0 +A CLI tool to manage Quartz applications + +USAGE: + quartz [SUBCOMMAND] + +OPTIONS: + -h, --help Print help information + -V, --version Print version information + +SUBCOMMANDS: + init Create base Quartz app directory from template + build Build the contract and enclave binaries + start Configure Gramine, sign, and start the enclave binary + deploy Deploy the WASM binary to the blockchain and call instantiate + run Run the enclave handler, and expose a public query server for users + handshake Run the handshake between the contract and enclave +``` + +## Installation + +To install Quartz, ensure you have Rust and Cargo installed. Then run: + +```shell +cargo install quartz +``` + +## Usage of subcommands + +### Init + +Initialize a new Quartz app directory structure with optional name and path arguments. + +#### Usage + +```shell +$ quartz init --help +quartz-init +Create base Quartz app directory from template + +USAGE: + quartz init [OPTIONS] + +OPTIONS: + -n, --name Set the name of the Quartz app [default: ] + -p, --path Set the path where the Quartz app will be created [default: .] + -h, --help Print help information +``` + +#### Example + +```shell +quartz init --name --path +``` + +This command will create the following directory structure at the specified path (or the current directory if no path is +provided): + +```shell +$ tree // -L 1 +apps/transfers/ +├── contracts/ +├── enclave/ +├── frontend/ +└── README.md +``` From f894c15b5610518af354e1b084d18f6602042e68 Mon Sep 17 00:00:00 2001 From: hu55a1n1 Date: Mon, 22 Jul 2024 15:30:56 +0200 Subject: [PATCH 3/7] Remove run command CLI README.md --- cli/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index 367aeb4d..8222af78 100644 --- a/cli/README.md +++ b/cli/README.md @@ -24,7 +24,6 @@ SUBCOMMANDS: build Build the contract and enclave binaries start Configure Gramine, sign, and start the enclave binary deploy Deploy the WASM binary to the blockchain and call instantiate - run Run the enclave handler, and expose a public query server for users handshake Run the handshake between the contract and enclave ``` From 84e15c896b4cce9376005cb4e01e531f7a693a73 Mon Sep 17 00:00:00 2001 From: hu55a1n1 Date: Mon, 22 Jul 2024 17:55:17 +0200 Subject: [PATCH 4/7] Update CLI README.md to add enclave & contract subcommands --- cli/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index 8222af78..b1385c57 100644 --- a/cli/README.md +++ b/cli/README.md @@ -22,8 +22,8 @@ OPTIONS: SUBCOMMANDS: init Create base Quartz app directory from template build Build the contract and enclave binaries - start Configure Gramine, sign, and start the enclave binary - deploy Deploy the WASM binary to the blockchain and call instantiate + enclave Enclave subcommads to configure Gramine, build, sign, and start the enclave binary + contract Contract subcommads to build, deploy the WASM binary to the blockchain and call instantiate handshake Run the handshake between the contract and enclave ``` From 9d1a9551a2ebd836421672afe900e9c99b1143c9 Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Mon, 22 Jul 2024 18:52:02 +0200 Subject: [PATCH 5/7] Add missing doc comment for `Init` CLI command Co-authored-by: Thane Thomson --- cli/src/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 285568ab..a48b570f 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -34,6 +34,7 @@ pub struct Cli { #[derive(Debug, Subcommand)] pub enum Command { + /// Create an empty Quartz app from a template Init { /// path to create & init a quartz app, defaults to current path if unspecified #[clap(long)] From aac78a7284766d5bc143641a0b17bcc395a957a6 Mon Sep 17 00:00:00 2001 From: hu55a1n1 Date: Mon, 22 Jul 2024 19:01:12 +0200 Subject: [PATCH 6/7] Add comment about design --- cli/src/main.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index b54cce99..ad12a297 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -41,7 +41,14 @@ fn main() -> Result<()> { .finish() .init(); + // The idea is to parse the input args and convert them into `Requests` which are + // correct-by-construction types that this tool can handle. All validation should happen during + // this conversion. let request = Request::try_from(args.command)?; + + // Each `Request` defines an associated `Handler` (i.e. logic) and `Response`. All handlers are + // free to log to the terminal and these logs are sent to `stderr`. + // `Handlers` must use `Responses` to output to `stdout`. request.handle(args.verbose)?; Ok(()) From 1448713a65ad07da8c2c56bfde8d44cb34384a71 Mon Sep 17 00:00:00 2001 From: hu55a1n1 Date: Mon, 22 Jul 2024 19:47:46 +0200 Subject: [PATCH 7/7] Print response --- Cargo.lock | 1 + Cargo.toml | 2 +- cli/Cargo.toml | 1 + cli/src/main.rs | 7 ++++++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6548d43..2f877a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2549,6 +2549,7 @@ dependencies = [ "color-eyre", "displaydoc", "serde", + "serde_json", "thiserror", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index d872644c..2d91f341 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ rand = { version = "0.8.5", default-features = false, features = ["getrandom"] } rand_core = { version = "0.6", default-features = false, features = ["std"] } reqwest = { version = "0.12.2", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0.203", default-features = false, features = ["derive"] } -serde_json = { version = "1.0.94", default-features = false } +serde_json = { version = "1.0.94", default-features = false, features = ["alloc"] } serde_with = { version = "3.4.0", default-features = false, features = ["hex", "macros"] } sha2 = { version = "0.10.8", default-features = false } subtle-encoding = { version = "0.5.1", default-features = false, features = ["bech32-preview"] } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 75038ddf..606b5c06 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -14,6 +14,7 @@ clap.workspace = true color-eyre.workspace = true displaydoc.workspace = true serde.workspace = true +serde_json.workspace = true thiserror.workspace = true tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/cli/src/main.rs b/cli/src/main.rs index ad12a297..9288ca94 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -48,8 +48,13 @@ fn main() -> Result<()> { // Each `Request` defines an associated `Handler` (i.e. logic) and `Response`. All handlers are // free to log to the terminal and these logs are sent to `stderr`. + let response = request.handle(args.verbose)?; + // `Handlers` must use `Responses` to output to `stdout`. - request.handle(args.verbose)?; + println!( + "{}", + serde_json::to_string(&response).expect("infallible serializer") + ); Ok(()) }