#![deny(unused_crate_dependencies)]
mod cmd;
mod crate_metadata;
mod util;
mod validate_wasm;
mod wasm_opt;
mod workspace;
use self::{
cmd::{
metadata::MetadataResult,
BuildCommand,
CallCommand,
CheckCommand,
DecodeCommand,
ErrorVariant,
InstantiateCommand,
TestCommand,
UploadCommand,
},
util::DEFAULT_KEY_COL_WIDTH,
workspace::ManifestPath,
};
use std::{
convert::TryFrom,
fmt::{
Display,
Formatter,
Result as DisplayResult,
},
path::PathBuf,
str::FromStr,
};
use ::wasm_opt::OptimizationOptions;
use anyhow::{
anyhow,
Error,
Result,
};
use clap::{
Args,
Parser,
Subcommand,
};
use colored::Colorize;
#[cfg(test)]
use assert_cmd as _;
#[cfg(test)]
use predicates as _;
#[cfg(test)]
use regex as _;
use which as _;
#[derive(Debug, Parser)]
#[clap(bin_name = "cargo")]
#[clap(version = env!("CARGO_CONTRACT_CLI_IMPL_VERSION"))]
pub(crate) enum Opts {
#[clap(name = "contract")]
#[clap(version = env!("CARGO_CONTRACT_CLI_IMPL_VERSION"))]
#[clap(action = ArgAction::DeriveDisplayOrder)]
Contract(ContractArgs),
}
#[derive(Debug, Args)]
pub(crate) struct ContractArgs {
#[clap(subcommand)]
cmd: Command,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct HexData(pub Vec<u8>);
impl FromStr for HexData {
type Err = hex::FromHexError;
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
hex::decode(input).map(HexData)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum OptimizationPasses {
Zero,
One,
Two,
Three,
Four,
S,
Z,
}
impl Display for OptimizationPasses {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
let out = match self {
OptimizationPasses::Zero => "0",
OptimizationPasses::One => "1",
OptimizationPasses::Two => "2",
OptimizationPasses::Three => "3",
OptimizationPasses::Four => "4",
OptimizationPasses::S => "s",
OptimizationPasses::Z => "z",
};
write!(f, "{}", out)
}
}
impl Default for OptimizationPasses {
fn default() -> OptimizationPasses {
OptimizationPasses::Z
}
}
impl FromStr for OptimizationPasses {
type Err = Error;
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
let normalized_input = input.replace('"', "").to_lowercase();
match normalized_input.as_str() {
"0" => Ok(OptimizationPasses::Zero),
"1" => Ok(OptimizationPasses::One),
"2" => Ok(OptimizationPasses::Two),
"3" => Ok(OptimizationPasses::Three),
"4" => Ok(OptimizationPasses::Four),
"s" => Ok(OptimizationPasses::S),
"z" => Ok(OptimizationPasses::Z),
_ => anyhow::bail!("Unknown optimization passes for option {}", input),
}
}
}
impl From<String> for OptimizationPasses {
fn from(str: String) -> Self {
OptimizationPasses::from_str(&str).expect("conversion failed")
}
}
impl From<OptimizationPasses> for OptimizationOptions {
fn from(passes: OptimizationPasses) -> OptimizationOptions {
match passes {
OptimizationPasses::Zero => OptimizationOptions::new_opt_level_0(),
OptimizationPasses::One => OptimizationOptions::new_opt_level_1(),
OptimizationPasses::Two => OptimizationOptions::new_opt_level_2(),
OptimizationPasses::Three => OptimizationOptions::new_opt_level_3(),
OptimizationPasses::Four => OptimizationOptions::new_opt_level_4(),
OptimizationPasses::S => OptimizationOptions::new_optimize_for_size(),
OptimizationPasses::Z => {
OptimizationOptions::new_optimize_for_size_aggressively()
}
}
}
}
#[derive(Default, Clone, Debug, Args)]
pub struct VerbosityFlags {
#[clap(long)]
quiet: bool,
#[clap(long)]
verbose: bool,
}
#[derive(Clone, Copy, serde::Serialize, Eq, PartialEq)]
pub enum Verbosity {
Default,
Quiet,
Verbose,
}
impl Default for Verbosity {
fn default() -> Self {
Verbosity::Default
}
}
impl Verbosity {
pub(crate) fn is_verbose(&self) -> bool {
match self {
Verbosity::Quiet => false,
Verbosity::Default | Verbosity::Verbose => true,
}
}
}
impl TryFrom<&VerbosityFlags> for Verbosity {
type Error = Error;
fn try_from(value: &VerbosityFlags) -> Result<Self, Self::Error> {
match (value.quiet, value.verbose) {
(false, false) => Ok(Verbosity::Default),
(true, false) => Ok(Verbosity::Quiet),
(false, true) => Ok(Verbosity::Verbose),
(true, true) => anyhow::bail!("Cannot pass both --quiet and --verbose flags"),
}
}
}
#[derive(Default, Clone, Debug, Args)]
struct UnstableOptions {
#[clap(long = "unstable-options", short = 'Z', number_of_values = 1)]
options: Vec<String>,
}
#[derive(Clone, Default)]
struct UnstableFlags {
original_manifest: bool,
}
impl TryFrom<&UnstableOptions> for UnstableFlags {
type Error = Error;
fn try_from(value: &UnstableOptions) -> Result<Self, Self::Error> {
let valid_flags = ["original-manifest"];
let invalid_flags = value
.options
.iter()
.filter(|o| !valid_flags.contains(&o.as_str()))
.collect::<Vec<_>>();
if !invalid_flags.is_empty() {
anyhow::bail!("Unknown unstable-options {:?}", invalid_flags)
}
Ok(UnstableFlags {
original_manifest: value.options.contains(&"original-manifest".to_owned()),
})
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, clap::ValueEnum, serde::Serialize)]
#[clap(name = "build-artifacts")]
pub enum BuildArtifacts {
#[clap(name = "all")]
All,
#[clap(name = "code-only")]
CodeOnly,
#[clap(name = "check-only")]
CheckOnly,
}
impl BuildArtifacts {
pub fn steps(&self) -> BuildSteps {
match self {
BuildArtifacts::All => BuildSteps::new(5),
BuildArtifacts::CodeOnly => BuildSteps::new(4),
BuildArtifacts::CheckOnly => BuildSteps::new(1),
}
}
}
impl Default for BuildArtifacts {
fn default() -> Self {
BuildArtifacts::All
}
}
#[derive(Debug, Clone, Copy)]
pub struct BuildSteps {
pub current_step: usize,
pub total_steps: usize,
}
impl BuildSteps {
pub fn new(total_steps: usize) -> Self {
Self {
current_step: 1,
total_steps,
}
}
pub fn increment_current(&mut self) {
self.current_step += 1;
}
}
impl Display for BuildSteps {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
write!(f, "[{}/{}]", self.current_step, self.total_steps)
}
}
#[derive(Eq, PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum BuildMode {
Debug,
Release,
}
impl Default for BuildMode {
fn default() -> BuildMode {
BuildMode::Debug
}
}
impl Display for BuildMode {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
match self {
Self::Debug => write!(f, "debug"),
Self::Release => write!(f, "release"),
}
}
}
#[derive(Eq, PartialEq, Copy, Clone, Debug, serde::Serialize)]
pub enum Network {
Online,
Offline,
}
impl Default for Network {
fn default() -> Network {
Network::Online
}
}
impl Display for Network {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
match self {
Self::Online => write!(f, ""),
Self::Offline => write!(f, "--offline"),
}
}
}
pub enum OutputType {
HumanReadable,
Json,
}
impl Default for OutputType {
fn default() -> Self {
OutputType::HumanReadable
}
}
#[derive(serde::Serialize)]
pub struct BuildResult {
pub dest_wasm: Option<PathBuf>,
pub metadata_result: Option<MetadataResult>,
pub target_directory: PathBuf,
pub optimization_result: Option<OptimizationResult>,
pub build_mode: BuildMode,
pub build_artifact: BuildArtifacts,
pub verbosity: Verbosity,
#[serde(skip_serializing)]
pub output_type: OutputType,
}
#[derive(serde::Serialize)]
pub struct OptimizationResult {
pub dest_wasm: PathBuf,
pub original_size: f64,
pub optimized_size: f64,
}
impl BuildResult {
pub fn display(&self) -> String {
let optimization = self.display_optimization();
let size_diff = format!(
"\nOriginal wasm size: {}, Optimized: {}\n\n",
format!("{:.1}K", optimization.0).bold(),
format!("{:.1}K", optimization.1).bold(),
);
debug_assert!(
optimization.1 > 0.0,
"optimized file size must be greater 0"
);
let build_mode = format!(
"The contract was built in {} mode.\n\n",
format!("{}", self.build_mode).to_uppercase().bold(),
);
if self.build_artifact == BuildArtifacts::CodeOnly {
let out = format!(
"{}{}Your contract's code is ready. You can find it here:\n{}",
size_diff,
build_mode,
self.dest_wasm
.as_ref()
.expect("wasm path must exist")
.display()
.to_string()
.bold()
);
return out
};
let mut out = format!(
"{}{}Your contract artifacts are ready. You can find them in:\n{}\n\n",
size_diff,
build_mode,
self.target_directory.display().to_string().bold(),
);
if let Some(metadata_result) = self.metadata_result.as_ref() {
let bundle = format!(
" - {} (code + metadata)\n",
util::base_name(&metadata_result.dest_bundle).bold()
);
out.push_str(&bundle);
}
if let Some(dest_wasm) = self.dest_wasm.as_ref() {
let wasm = format!(
" - {} (the contract's code)\n",
util::base_name(dest_wasm).bold()
);
out.push_str(&wasm);
}
if let Some(metadata_result) = self.metadata_result.as_ref() {
let metadata = format!(
" - {} (the contract's metadata)",
util::base_name(&metadata_result.dest_metadata).bold()
);
out.push_str(&metadata);
}
out
}
fn display_optimization(&self) -> (f64, f64) {
let optimization = self
.optimization_result
.as_ref()
.expect("optimization result must exist");
(optimization.original_size, optimization.optimized_size)
}
pub fn serialize_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
}
#[derive(Debug, Subcommand)]
enum Command {
#[clap(name = "new")]
New {
name: String,
#[clap(short, long, value_parser)]
target_dir: Option<PathBuf>,
},
#[clap(name = "build")]
Build(BuildCommand),
#[clap(name = "check")]
Check(CheckCommand),
#[clap(name = "test")]
Test(TestCommand),
#[clap(name = "upload")]
Upload(UploadCommand),
#[clap(name = "instantiate")]
Instantiate(InstantiateCommand),
#[clap(name = "call")]
Call(CallCommand),
#[clap(name = "decode")]
Decode(DecodeCommand),
}
fn main() {
tracing_subscriber::fmt::init();
let Opts::Contract(args) = Opts::parse();
match exec(args.cmd) {
Ok(()) => {}
Err(err) => {
eprintln!("{}", err);
std::process::exit(1);
}
}
}
fn exec(cmd: Command) -> Result<()> {
match &cmd {
Command::New { name, target_dir } => {
cmd::new::execute(name, target_dir.as_ref())?;
println!("Created contract {}", name);
Ok(())
}
Command::Build(build) => {
let result = build.exec().map_err(format_err)?;
if matches!(result.output_type, OutputType::Json) {
println!("{}", result.serialize_json()?)
} else if result.verbosity.is_verbose() {
println!("{}", result.display())
}
Ok(())
}
Command::Check(check) => {
let res = check.exec().map_err(format_err)?;
assert!(
res.dest_wasm.is_none(),
"no dest_wasm must be on the generation result"
);
if res.verbosity.is_verbose() {
println!("\nYour contract's code was built successfully.")
}
Ok(())
}
Command::Test(test) => {
let res = test.exec().map_err(format_err)?;
if res.verbosity.is_verbose() {
println!("{}", res.display()?)
}
Ok(())
}
Command::Upload(upload) => {
upload
.run()
.map_err(|err| map_extrinsic_err(err, upload.is_json()))
}
Command::Instantiate(instantiate) => {
instantiate
.run()
.map_err(|err| map_extrinsic_err(err, instantiate.is_json()))
}
Command::Call(call) => {
call.run()
.map_err(|err| map_extrinsic_err(err, call.is_json()))
}
Command::Decode(decode) => decode.run().map_err(format_err),
}
}
fn map_extrinsic_err(err: ErrorVariant, is_json: bool) -> Error {
if is_json {
anyhow!(
"{}",
serde_json::to_string_pretty(&err)
.expect("error serialization is infallible; qed")
)
} else {
format_err(err)
}
}
fn format_err<E: Display>(err: E) -> Error {
anyhow!(
"{} {}",
"ERROR:".bright_red().bold(),
format!("{}", err).bright_red()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_result_seralization_sanity_check() {
let raw_result = r#"{
"dest_wasm": "/path/to/contract.wasm",
"metadata_result": {
"dest_metadata": "/path/to/metadata.json",
"dest_bundle": "/path/to/contract.contract"
},
"target_directory": "/path/to/target",
"optimization_result": {
"dest_wasm": "/path/to/contract.wasm",
"original_size": 64.0,
"optimized_size": 32.0
},
"build_mode": "Debug",
"build_artifact": "All",
"verbosity": "Quiet"
}"#;
let build_result = BuildResult {
dest_wasm: Some(PathBuf::from("/path/to/contract.wasm")),
metadata_result: Some(MetadataResult {
dest_metadata: PathBuf::from("/path/to/metadata.json"),
dest_bundle: PathBuf::from("/path/to/contract.contract"),
}),
target_directory: PathBuf::from("/path/to/target"),
optimization_result: Some(OptimizationResult {
dest_wasm: PathBuf::from("/path/to/contract.wasm"),
original_size: 64.0,
optimized_size: 32.0,
}),
build_mode: Default::default(),
build_artifact: Default::default(),
verbosity: Verbosity::Quiet,
output_type: OutputType::Json,
};
let serialized_result = build_result.serialize_json();
assert!(serialized_result.is_ok());
assert_eq!(serialized_result.unwrap(), raw_result);
}
}