Commit 7358ca80 authored by Andrew Jones's avatar Andrew Jones Committed by Hero Bird

[cli] remove cargo-contract (moved to https://github.com/paritytech/cargo-contract) (#278)

* Remove cli (moved to https://github.com/paritytech/cargo-contract)

* Add link to cargo-contract repo

* Update GitLab CI to no longer consider cli

* Remove `cli` from the list of workspace members
parent 72c1f187
Pipeline #70032 passed with stages
in 9 minutes and 11 seconds
......@@ -17,8 +17,7 @@ variables:
CARGO_INCREMENTAL: 0
CI_SERVER_NAME: "GitLab CI"
REGISTRY: registry.parity.io/parity/infrastructure/scripts
ALL_CRATES: "core alloc utils lang2 lang2/macro cli"
WASM_CRATES: "core alloc utils lang2 lang2/macro"
ALL_CRATES: "core alloc utils lang2 lang2/macro"
.collect-artifacts: &collect-artifacts
artifacts:
......@@ -69,7 +68,7 @@ check-wasm:
stage: check
<<: *docker-env
script:
- for crate in ${WASM_CRATES}; do
- for crate in ${ALL_CRATES}; do
cargo check --verbose --no-default-features --target wasm32-unknown-unknown --manifest-path ${crate}/Cargo.toml;
done
......@@ -91,7 +90,7 @@ build-wasm:
dependencies:
- check-wasm
script:
- for crate in ${WASM_CRATES}; do
- for crate in ${ALL_CRATES}; do
cargo build --verbose --no-default-features --release --target wasm32-unknown-unknown --manifest-path ${crate}/Cargo.toml;
done
......@@ -121,7 +120,7 @@ clippy-wasm:
dependencies:
- check-wasm
script:
- for crate in ${WASM_CRATES}; do
- for crate in ${ALL_CRATES}; do
cargo clippy --verbose --manifest-path ${crate}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings;
done
......
......@@ -6,12 +6,10 @@ members = [
"model",
"lang",
"lang2",
"cli",
"abi",
]
exclude = [
"examples/",
"cli/template/",
]
[profile.release]
......
......@@ -37,10 +37,10 @@ Use the scripts provided under `scripts` directory in order to run checks on eit
## Examples
For building the example smart contracts found under `examples` you will need to have `cargo-contract` installed.
For building the example smart contracts found under `examples` you will need to have [`cargo-contract`](https://github.com/paritytech/cargo-contract) installed.
```
cargo install --git https://github.com/paritytech/ink cargo-contract --force
cargo install --git https://github.com/paritytech/cargo-contract cargo-contract --force
```
Use the `--force` to ensure you are updated to the most recent `cargo-contract` version.
......
......@@ -22,7 +22,7 @@ Follow the instructions below to understand how to migrate your ink! 1.0 contrac
Install the latest ink! CLI using the following command:
```bash
cargo install --git https://github.com/paritytech/ink cargo-contract --force
cargo install --git https://github.com/paritytech/cargo-contract cargo-contract --force
```
There is a new contract metadata format you need to use. You can generate the metadata using:
......
[package]
name = "cargo-contract"
version = "0.2.0"
authors = ["Parity Technologies <admin@parity.io>"]
build = "build.rs"
edition = "2018"
license = "APACHE-2.0"
readme = "README.md"
repository = "https://github.com/paritytech/ink"
documentation = "https://substrate.dev/substrate-contracts-workshop/#/"
homepage = "https://www.parity.io/"
description = "Setup and deployment tool for developing Wasm based smart contracts via ink!"
keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl"]
categories = ["cli", "tool"]
include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"]
[dependencies]
env_logger = "0.7"
derive_more = { version = "0.99.2", default-features = false, features = ["from", "display"] }
structopt = "0.3"
itertools = "0.8"
log = "0.4"
heck = "0.3"
futures = "0.1.28"
jsonrpc-core-client = { version = "14.0", features = ["ws"] }
zip = { version = "0.5", default-features = false }
pwasm-utils = "0.12"
parity-wasm = "0.41"
cargo_metadata = "0.9"
substrate-primitives = { git = "https://github.com/paritytech/substrate/", package = "substrate-primitives" }
subxt = { git = "https://github.com/paritytech/substrate-subxt/", branch = "v0.3", package = "substrate-subxt" }
tokio = "0.1.21"
url = "1.7"
[build-dependencies]
zip = { version = "0.5", default-features = false }
walkdir = "2.2"
[dev-dependencies]
assert_matches = "1.3.0"
tempfile = "3.1.0"
wabt = "0.9.0"
[features]
default = []
test-ci-only = []
# Cargo plugin for Ink contracts
A small CLI tool for helping setting up and managing WebAssembly smart contracts written with ink!.
// Copyright 2018-2019 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
error::Error,
fs::File,
io::{
prelude::*,
Write,
},
iter::Iterator,
path::{
Path,
PathBuf,
},
result::Result,
};
use walkdir::WalkDir;
use zip::{
result::ZipError,
write::FileOptions,
CompressionMethod,
ZipWriter,
};
const DEFAULT_UNIX_PERMISSIONS: u32 = 0o755;
fn main() {
let src_dir = PathBuf::from("./template");
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR should be set by cargo");
let dst_file = Path::new(&out_dir).join("template.zip");
match zip_dir(&src_dir, &dst_file, CompressionMethod::Stored) {
Ok(_) => {
println!(
"done: {} written to {}",
src_dir.display(),
dst_file.display()
)
}
Err(e) => eprintln!("Error: {:?}", e),
};
}
fn zip_dir(
src_dir: &PathBuf,
dst_file: &PathBuf,
method: CompressionMethod,
) -> Result<(), Box<dyn Error>> {
if !src_dir.is_dir() {
return Err(ZipError::FileNotFound.into())
}
let file = File::create(dst_file)?;
let walkdir = WalkDir::new(src_dir);
let it = walkdir.into_iter().filter_map(|e| e.ok());
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(method)
.unix_permissions(DEFAULT_UNIX_PERMISSIONS);
let mut buffer = Vec::new();
for entry in it {
let path = entry.path();
let name = path.strip_prefix(&src_dir)?;
if path.is_file() {
zip.start_file_from_path(name, options)?;
let mut f = File::open(path)?;
f.read_to_end(&mut buffer)?;
zip.write_all(&*buffer)?;
buffer.clear();
} else if name.as_os_str().len() != 0 {
zip.add_directory_from_path(name, options)?;
}
}
zip.finish()?;
Ok(())
}
// Copyright 2018-2019 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::path::PathBuf;
use cargo_metadata::MetadataCommand;
use parity_wasm::elements::{
External,
MemoryType,
Module,
Section,
};
use crate::cmd::{
CommandError as Error,
Result,
};
/// This is the maximum number of pages available for a contract to allocate.
const MAX_MEMORY_PAGES: u32 = 16;
/// Relevant metadata obtained from Cargo.toml.
pub struct CrateMetadata {
original_wasm: PathBuf,
dest_wasm: PathBuf,
}
impl CrateMetadata {
/// Get the path of the wasm destination file
pub fn dest_wasm(self) -> PathBuf {
self.dest_wasm
}
}
/// Parses the contract manifest and returns relevant metadata.
pub fn collect_crate_metadata(working_dir: Option<&PathBuf>) -> Result<CrateMetadata> {
let mut cmd = MetadataCommand::new();
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
let metadata = cmd.exec()?;
let root_package_id = metadata
.resolve
.and_then(|resolve| resolve.root)
.ok_or_else(|| Error::Other("Cannot infer the root project id".to_string()))?;
// Find the root package by id in the list of packages. It is logical error if the root
// package is not found in the list.
let root_package = metadata
.packages
.iter()
.find(|package| package.id == root_package_id)
.expect("The package is not found in the `cargo metadata` output");
// Normalize the package name.
let package_name = root_package.name.replace("-", "_");
// {target_dir}/wasm32-unknown-unknown/release/{package_name}.wasm
let mut original_wasm = metadata.target_directory.clone();
original_wasm.push("wasm32-unknown-unknown");
original_wasm.push("release");
original_wasm.push(package_name.clone());
original_wasm.set_extension("wasm");
// {target_dir}/{package_name}.wasm
let mut dest_wasm = metadata.target_directory.clone();
dest_wasm.push(package_name);
dest_wasm.set_extension("wasm");
Ok(CrateMetadata {
original_wasm,
dest_wasm,
})
}
/// Invokes `cargo build` in the specified directory, defaults to the current directory.
///
/// Currently it assumes that user wants to use `+nightly`.
fn build_cargo_project(working_dir: Option<&PathBuf>) -> Result<()> {
super::exec_cargo(
"build",
&[
"--no-default-features",
"--release",
"--target=wasm32-unknown-unknown",
"--verbose",
],
working_dir,
)
}
/// Ensures the wasm memory import of a given module has the maximum number of pages.
///
/// Iterates over the import section, finds the memory import entry if any and adjusts the maximum
/// limit.
fn ensure_maximum_memory_pages(
module: &mut Module,
maximum_allowed_pages: u32,
) -> Result<()> {
let mem_ty = module
.import_section_mut()
.and_then(|section| {
section.entries_mut()
.iter_mut()
.find_map(|entry| {
match entry.external_mut() {
External::Memory(ref mut mem_ty) => Some(mem_ty),
_ => None,
}
})
})
.ok_or_else(||
Error::Other(
"Memory import is not found. Is --import-memory specified in the linker args".to_string()
)
)?;
if let Some(requested_maximum) = mem_ty.limits().maximum() {
// The module already has maximum, check if it is within the limit bail out.
if requested_maximum > maximum_allowed_pages {
return Err(
Error::Other(
format!(
"The wasm module requires {} pages. The maximum allowed number of pages is {}",
requested_maximum,
maximum_allowed_pages,
)
)
);
}
} else {
let initial = mem_ty.limits().initial();
*mem_ty = MemoryType::new(initial, Some(MAX_MEMORY_PAGES));
}
Ok(())
}
/// Strips all custom sections.
///
/// Presently all custom sections are not required so they can be stripped safely.
fn strip_custom_sections(module: &mut Module) {
module.sections_mut().retain(|section| {
match section {
Section::Custom(_) => false,
Section::Name(_) => false,
Section::Reloc(_) => false,
_ => true,
}
});
}
/// Performs required post-processing steps on the wasm artifact.
fn post_process_wasm(crate_metadata: &CrateMetadata) -> Result<()> {
// Deserialize wasm module from a file.
let mut module = parity_wasm::deserialize_file(&crate_metadata.original_wasm)?;
// Perform optimization.
//
// In practice only tree-shaking is performed, i.e transitively removing all symbols that are
// NOT used by the specified entrypoints.
pwasm_utils::optimize(&mut module, ["call", "deploy"].to_vec())?;
ensure_maximum_memory_pages(&mut module, MAX_MEMORY_PAGES)?;
strip_custom_sections(&mut module);
parity_wasm::serialize_to_file(&crate_metadata.dest_wasm, module)?;
Ok(())
}
/// Executes build of the smart-contract which produces a wasm binary that is ready for deploying.
///
/// It does so by invoking build by cargo and then post processing the final binary.
pub(crate) fn execute_build(working_dir: Option<&PathBuf>) -> Result<String> {
println!(" [1/3] Collecting crate metadata");
let crate_metadata = collect_crate_metadata(working_dir)?;
println!(" [2/3] Building cargo project");
build_cargo_project(working_dir)?;
println!(" [3/3] Post processing wasm file");
post_process_wasm(&crate_metadata)?;
Ok(format!(
"Your contract is ready.\nYou can find it here:\n{}",
crate_metadata.dest_wasm.display()
))
}
#[cfg(test)]
mod tests {
use crate::{
cmd::{
execute_new,
tests::with_tmp_dir,
},
AbstractionLayer,
};
#[cfg(feature = "test-ci-only")]
#[test]
fn build_template() {
with_tmp_dir(|path| {
execute_new(AbstractionLayer::Lang, "new_project", Some(path))
.expect("new project creation failed");
super::execute_build(Some(&path.join("new_project"))).expect("build failed");
});
}
}
// Copyright 2018-2019 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
fs,
io::Read,
path::PathBuf,
};
use futures::future::Future;
use substrate_primitives::{
crypto::Pair,
sr25519,
H256,
};
use subxt::{
contracts,
system::System,
DefaultNodeRuntime,
};
use crate::cmd::{
build::{
self,
CrateMetadata,
},
Result,
};
/// Load the wasm blob from the specified path.
///
/// Defaults to the target contract wasm in the current project, inferred via the crate metadata.
fn load_contract_code(path: Option<&PathBuf>) -> Result<Vec<u8>> {
let default_wasm_path =
build::collect_crate_metadata(path).map(CrateMetadata::dest_wasm)?;
let contract_wasm_path = path.unwrap_or(&default_wasm_path);
let mut data = Vec::new();
let mut file = fs::File::open(&contract_wasm_path)
.map_err(|e| format!("Failed to open {}: {}", contract_wasm_path.display(), e))?;
file.read_to_end(&mut data)?;
Ok(data)
}
/// Attempt to extract the code hash from the extrinsic result.
///
/// Returns an Error if the `Contracts::CodeStored` is not found or cannot be decoded.
fn extract_code_hash<T: System>(
extrinsic_result: subxt::ExtrinsicSuccess<T>,
) -> Result<H256> {
match extrinsic_result.find_event::<H256>("Contracts", "CodeStored") {
Some(Ok(hash)) => Ok(hash),
Some(Err(err)) => Err(format!("Failed to decode code hash: {}", err).into()),
None => Err("Failed to find Contracts::CodeStored Event".into()),
}
}
/// Put contract code to a smart contract enabled substrate chain.
/// Returns the code hash of the deployed contract if successful.
///
/// Optionally supply the contract wasm path, defaults to destination contract file inferred from
/// Cargo.toml of the current contract project.
///
/// Creates an extrinsic with the `Contracts::put_code` Call, submits via RPC, then waits for
/// the `ContractsEvent::CodeStored` event.
pub(crate) fn execute_deploy(
url: url::Url,
suri: &str,
password: Option<&str>,
gas: u64,
contract_wasm_path: Option<&PathBuf>,
) -> Result<String> {
let signer = sr25519::Pair::from_string(suri, password)?;
let code = load_contract_code(contract_wasm_path)?;
let fut = subxt::ClientBuilder::<DefaultNodeRuntime>::new()
.set_url(url)
.build()
.and_then(|cli| cli.xt(signer, None))
.and_then(move |xt| xt.submit_and_watch(contracts::put_code(gas, code)));
let mut rt = tokio::runtime::Runtime::new()?;
let extrinsic_success = rt.block_on(fut)?;
log::debug!("Deploy success: {:?}", extrinsic_success);
let code_hash = extract_code_hash(extrinsic_success)?;
Ok(format!("Code hash: {:?}", code_hash))
}
#[cfg(test)]
mod tests {
use std::{
fs,
io::Write,
path,
};
use assert_matches::assert_matches;
#[test]
#[ignore] // depends on a local substrate node running
fn deploy_contract() {
const CONTRACT: &str = r#"
(module
(func (export "call"))
(func (export "deploy"))
)
"#;
let wasm = wabt::wat2wasm(CONTRACT).expect("invalid wabt");
let out_dir = path::Path::new(env!("OUT_DIR"));
let target_dir = path::Path::new("./target");
let _ = fs::create_dir(target_dir);
let wasm_path = out_dir.join("flipper-pruned.wasm");
let mut file = fs::File::create(&wasm_path).unwrap();
let _ = file.write_all(&wasm);
let url = url::Url::parse("ws://localhost:9944").unwrap();
let result =
super::execute_deploy(url, "//Alice", None, 500_000, Some(&wasm_path));
assert_matches!(result, Ok(_));
}
}
// Copyright 2018-2019 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
io::Error as IoError,
result::Result as StdResult,
};
use jsonrpc_core_client::RpcError;
use substrate_primitives::crypto::SecretStringError;
use subxt::Error as SubXtError;
use zip::result::ZipError;
/// An error that can be encountered while executing commands.
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum CommandError {
Io(IoError),
#[display(fmt = "Command unimplemented")]
UnimplementedCommand,
#[display(fmt = "Abstraction layer unimplemented")]
UnimplementedAbstractionLayer,
Rpc(RpcError),
#[display(fmt = "Secret string error")]
SecretString(SecretStringError),
SubXt(SubXtError),
ZipError(ZipError),
BuildFailed,
#[display(fmt = "Error invoking `cargo metadata`")]
CargoMetadata(cargo_metadata::Error),
WasmDeserialization(parity_wasm::elements::Error),
#[display(fmt = "Optimizer failed")]
Optimizer(pwasm_utils::OptimizerError),
Other(String),
}
impl From<&str> for CommandError {
fn from(error: &str) -> Self {
CommandError::Other(error.into())
}
}
/// Result type that has a `CommandError`.
pub type Result<T> = StdResult<T, CommandError>;
// Copyright 2018-2019 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.