Commit e5d03264 authored by Andrew Jones's avatar Andrew Jones Committed by Hero Bird

[cli] deploy command (#131)

* [cli] construct put_code extrinsic

* [cli] fetch current account nonce

* [cli] submit extrinsic skeleton

* [cli] submit extrinsic compiles and works?

* [cli] refactor futures

* [cli] extract substrate rpc module

* [cli] Encapsulate rpc calls in struct

* [cli] extract rpc call methods

* [cli] temporarily add patch for client pub/sub

* [cli] generic Hash

* [cli] back to explicit H256

* [cli] update parity-codec

* [cli] add methods to submit and watch, and fetch events

* [cli] update to paritytech jsonrpc repo patch

* [cli] switch to ws transport

* [cli] WIP: return extrinsic result with code hash

* [cli] subscribe to block events

* [cli] use jsonrpc 12.1 rather than crates,io patch

* [cli] add logging for troubleshooting

* [cli] deserialize OpaqueExtrinsic properly, logging for ext hash

* [cli] still trying to get the Events for the block

* [cli] log events storage key for debugging

* [cli] refactor: function to extract extrinsic events

* [cli] move extract code hash to deploy

* ignore rust-toolchain file used for local builds

* Update to new extrinsic format with extra

* Fix signature with extra

* Make System Events subscription work

* Refactor EventRecord decoding

* Extract code hash from events

* rustfmt

* Refactor: extract functions from Author

* Remove stuff from .gitignore which could go in .git/info/exclude

* Remove ws::connect unwraps

* Pass in url from cli flag

* Combine all rpc interfaces

* Consistency in log messages

* Expand short arg names

* rustfmt

* Comments and refactoring

* rustfmt

* Pass in secret in from command line

* Change default url to ws

* Add integration test (not passing yet) for deploy command

* Extracted substrate extrinsic RPC to subxt

* Get it working with subxt

* [cli] update to latest subxt

* [cli] convert to new subxt

* [cli] add CheckVersion to SignedExtra type

* Use latest version of subxt with dynamic events

* [cli] cargo fmt

* [cli] deploy: remove dependency on node-runtime!

* [cli] use assert_matches in test

* [cli] reuse crate metadata fn from build command to get wasm path

* [cli] add comments

* [cli] remove unused dependencies

* [cli] remove more unused dependencies

* [cli] Use static method

* [cli] doc comments

* [cli] rename surl -> suri

* [cli] move Runtime defintion to subxt, reduce direct substrate deps

* [cli] rustfmt
parent 89dca458
Pipeline #53320 failed with stages
in 5 minutes and 5 seconds
......@@ -13,11 +13,5 @@
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
# Ignore VS Code artifacts.
**/.vscode/**
# Ignore idea artifacts.
**/.idea/**
# Ignore history files.
**/.history/**
......@@ -15,21 +15,31 @@ categories = ["cli", "tool"]
include = ["/Cargo.toml", "src/**/*.rs", "/README.md", "/LICENSE"]
[dependencies]
env_logger = "0.6"
derive_more = "0.14.0"
structopt = "0.2.15"
itertools = "0.8"
log = "0.4"
heck = "0.3"
futures = "0.1.28"
jsonrpc-core-client = { version = "13.0", features = ["ws"] }
zip = { version = "0.5", default-features = false }
pwasm-utils = "0.11.0"
parity-wasm = "0.40.2"
cargo_metadata = "0.8.2"
substrate-primitives = { git = "https://github.com/paritytech/substrate/", package = "substrate-primitives" }
subxt = { git = "https://github.com/paritytech/substrate-subxt/", package = "substrate-subxt" }
tokio = "0.1.21"
url = "1.7"
[build-dependencies]
zip = { version = "0.5", default-features = false }
walkdir = "1.0"
[dev-dependencies]
assert_matches = "1.3.0"
tempfile = "3.1.0"
wabt = "0.9.0"
[features]
default = []
......
......@@ -38,13 +38,20 @@ use std::{
const MAX_MEMORY_PAGES: u32 = 16;
/// Relevant metadata obtained from Cargo.toml.
struct CrateMetadata {
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.
fn collect_crate_metadata() -> Result<CrateMetadata> {
pub fn collect_crate_metadata() -> Result<CrateMetadata> {
let metadata = MetadataCommand::new().exec()?;
let root_package_id = metadata
......
// Copyright 2019 Parity Technologies (UK) Ltd.
// This file is part of ink!.
//
// ink! is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ink! is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ink!. If not, see <http://www.gnu.org/licenses/>.
use crate::cmd::{
build::{
self,
CrateMetadata,
},
Result,
};
use futures::future::Future;
use std::{
fs,
io::Read,
path::PathBuf,
};
use substrate_primitives::{
crypto::Pair,
sr25519,
H256,
};
use subxt::{
contracts::ContractsXt,
system::System,
DefaultNodeRuntime,
};
/// 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().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)?;
return 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.contracts(|call| call.put_code(gas, code))
.submit_and_watch()
});
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 assert_matches::assert_matches;
use std::{
fs,
io::Write,
path,
};
#[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(_));
}
}
......@@ -14,10 +14,13 @@
// You should have received a copy of the GNU General Public License
// along with ink!. If not, see <http://www.gnu.org/licenses/>.
use jsonrpc_core_client::RpcError;
use std::{
io::Error as IoError,
result::Result as StdResult,
};
use substrate_primitives::crypto::SecretStringError;
use subxt::Error as SubXtError;
use zip::result::ZipError;
/// An error that can be encountered while executing commands.
......@@ -28,6 +31,10 @@ pub enum CommandError {
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`")]
......
......@@ -15,11 +15,13 @@
// along with ink!. If not, see <http://www.gnu.org/licenses/>.
mod build;
mod deploy;
mod error;
mod new;
pub(crate) use self::{
build::execute_build,
deploy::execute_deploy,
error::{
CommandError,
Result,
......
......@@ -108,13 +108,8 @@ fn initialize_for_lang(name: &str) -> Result<String> {
pub(crate) fn execute_new(layer: AbstractionLayer, name: &str) -> Result<String> {
match layer {
AbstractionLayer::Core => {
Err(CommandError::UnimplementedAbstractionLayer)
}
AbstractionLayer::Model => {
Err(CommandError::UnimplementedAbstractionLayer)
}
AbstractionLayer::Core => Err(CommandError::UnimplementedAbstractionLayer),
AbstractionLayer::Model => Err(CommandError::UnimplementedAbstractionLayer),
AbstractionLayer::Lang => initialize_for_lang(name),
}
}
......
......@@ -20,6 +20,7 @@ use structopt::{
clap::AppSettings,
StructOpt,
};
use url::Url;
#[derive(Debug, StructOpt)]
#[structopt(bin_name = "cargo")]
......@@ -93,13 +94,32 @@ enum Command {
/// Deploy the smart contract on-chain. (Also for testing purposes.)
#[structopt(name = "deploy")]
Deploy {
/// Deploy on a local development chain.
#[structopt(name = "dev", short, long)]
on_dev: bool,
/// Websockets url of a substrate node
#[structopt(
name = "url",
long,
parse(try_from_str),
default_value = "ws://localhost:9944"
)]
url: Url,
/// Secret key URI for the account deploying the contract.
#[structopt(name = "suri", long, short)]
suri: String,
/// Password for the secret key
#[structopt(name = "password", long, short)]
password: Option<String>,
#[structopt(name = "gas", long, default_value = "500000")]
/// Maximum amount of gas to be used in this deployment
gas: u64,
/// Path to wasm contract code, defaults to ./target/<name>-pruned.wasm
#[structopt(parse(from_os_str))]
wasm_path: Option<std::path::PathBuf>,
},
}
fn main() {
env_logger::init();
let Opts::Contract(args) = Opts::from_args();
match exec(args.cmd) {
Ok(msg) => println!("\t{}", msg),
......@@ -113,6 +133,20 @@ fn exec(cmd: Command) -> cmd::Result<String> {
Command::New { layer, name } => cmd::execute_new(*layer, name),
Command::Build {} => cmd::execute_build(),
Command::Test {} => Err(CommandError::UnimplementedCommand),
Command::Deploy { .. } => Err(CommandError::UnimplementedCommand),
Command::Deploy {
url,
suri,
password,
gas,
wasm_path,
} => {
cmd::execute_deploy(
url.clone(),
suri,
password.as_ref().map(String::as_ref),
*gas,
wasm_path.as_ref(),
)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment