feat(cli): setup skeleton for quartz tool (#105)

Co-authored-by: Thane Thomson <connect@thanethomson.com>
This commit is contained in:
Shoaib Ahmed 2024-07-23 11:35:38 +02:00 committed by GitHub
parent a23ae1e560
commit 2afbe50f76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 332 additions and 1 deletions

14
Cargo.lock generated
View file

@ -2541,6 +2541,20 @@ dependencies = [
"prost", "prost",
] ]
[[package]]
name = "quartz"
version = "0.1.0"
dependencies = [
"clap",
"color-eyre",
"displaydoc",
"serde",
"serde_json",
"thiserror",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "quartz-app-mtcs-enclave" name = "quartz-app-mtcs-enclave"
version = "0.1.0" version = "0.1.0"

View file

@ -3,6 +3,7 @@ resolver = "2"
members = [ members = [
"apps/mtcs/enclave", "apps/mtcs/enclave",
"apps/transfers/enclave", "apps/transfers/enclave",
"cli",
"core/light-client-proofs/*", "core/light-client-proofs/*",
"core/quartz", "core/quartz",
"cosmwasm/packages/*", "cosmwasm/packages/*",
@ -39,7 +40,7 @@ rand = { version = "0.8.5", default-features = false, features = ["getrandom"] }
rand_core = { version = "0.6", default-features = false, features = ["std"] } rand_core = { version = "0.6", default-features = false, features = ["std"] }
reqwest = { version = "0.12.2", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12.2", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0.203", default-features = false, features = ["derive"] } 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"] } serde_with = { version = "3.4.0", default-features = false, features = ["hex", "macros"] }
sha2 = { version = "0.10.8", default-features = false } sha2 = { version = "0.10.8", default-features = false }
subtle-encoding = { version = "0.5.1", default-features = false, features = ["bech32-preview"] } subtle-encoding = { version = "0.5.1", default-features = false, features = ["bech32-preview"] }

20
cli/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[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
serde_json.workspace = true
thiserror.workspace = true
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }

76
cli/README.md Normal file
View file

@ -0,0 +1,76 @@
# 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
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
```
## 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 <NAME> Set the name of the Quartz app [default: <name of parent directory>]
-p, --path <PATH> Set the path where the Quartz app will be created [default: .]
-h, --help Print help information
```
#### Example
```shell
quartz init --name <app_name> --path <path>
```
This command will create the following directory structure at the specified path (or the current directory if no path is
provided):
```shell
$ tree /<path>/<app-name> -L 1
apps/transfers/
├── contracts/
├── enclave/
├── frontend/
└── README.md
```

43
cli/src/cli.rs Normal file
View file

@ -0,0 +1,43 @@
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 {
/// Create an empty Quartz app from a template
Init {
/// path to create & init a quartz app, defaults to current path if unspecified
#[clap(long)]
path: Option<PathBuf>,
},
}

8
cli/src/error.rs Normal file
View file

@ -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),
}

22
cli/src/handler.rs Normal file
View file

@ -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<Self::Response, Self::Error>;
}
impl Handler for Request {
type Error = Error;
type Response = Response;
fn handle(self, verbosity: Verbosity) -> Result<Self::Response, Self::Error> {
match self {
Request::Init(request) => request.handle(verbosity),
}
.map(Into::into)
}
}

16
cli/src/handler/init.rs Normal file
View file

@ -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<Self::Response, Self::Error> {
trace!("initializing directory structure...");
todo!()
}
}

60
cli/src/main.rs Normal file
View file

@ -0,0 +1,60 @@
#![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();
// 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`.
let response = request.handle(args.verbose)?;
// `Handlers` must use `Responses` to output to `stdout`.
println!(
"{}",
serde_json::to_string(&response).expect("infallible serializer")
);
Ok(())
}

19
cli/src/request.rs Normal file
View file

@ -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<Command> for Request {
type Error = Error;
fn try_from(cmd: Command) -> Result<Self, Self::Error> {
match cmd {
Command::Init { path } => InitRequest::try_from(path),
}
.map(Into::into)
}
}

30
cli/src/request/init.rs Normal file
View file

@ -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<Option<PathBuf>> for InitRequest {
type Error = Error;
fn try_from(path: Option<PathBuf>) -> Result<Self, Self::Error> {
if let Some(path) = path {
if !path.is_dir() {
return Err(Error::PathNotDir(format!("{}", path.display())));
}
}
todo!()
}
}
impl From<InitRequest> for Request {
fn from(request: InitRequest) -> Self {
Self::Init(request)
}
}

10
cli/src/response.rs Normal file
View file

@ -0,0 +1,10 @@
use serde::Serialize;
use crate::response::init::InitResponse;
pub mod init;
#[derive(Clone, Debug, Serialize)]
pub enum Response {
Init(InitResponse),
}

12
cli/src/response/init.rs Normal file
View file

@ -0,0 +1,12 @@
use serde::Serialize;
use crate::response::Response;
#[derive(Clone, Debug, Serialize)]
pub struct InitResponse;
impl From<InitResponse> for Response {
fn from(response: InitResponse) -> Self {
Self::Init(response)
}
}