diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc7326b2..f7bddbcec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased] +## [5.0.2] + ### Changed - Target `pallet-revive` instead of `pallet-contracts` - [#1851](https://github.com/use-ink/cargo-contract/pull/1851) +### Added +- Add `suri-path` and `password-path` options for `cargo contract` commands including `upload`, `instantiate`, `call`, and `remove` + ## [5.0.1] ### Changed diff --git a/Cargo.lock b/Cargo.lock index c6eb28cb9..02b850590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ [[package]] name = "cargo-contract" -version = "5.0.1" +version = "5.0.2" dependencies = [ "anyhow", "assert_cmd", @@ -1906,7 +1906,7 @@ checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" [[package]] name = "contract-analyze" -version = "5.0.1" +version = "5.0.2" dependencies = [ "anyhow", "contract-metadata", @@ -1916,7 +1916,7 @@ dependencies = [ [[package]] name = "contract-build" -version = "5.0.1" +version = "5.0.2" dependencies = [ "anyhow", "blake2", @@ -1958,7 +1958,7 @@ dependencies = [ [[package]] name = "contract-extrinsics" -version = "5.0.1" +version = "5.0.2" dependencies = [ "anyhow", "assert_cmd", @@ -1996,7 +1996,7 @@ dependencies = [ [[package]] name = "contract-metadata" -version = "5.0.1" +version = "5.0.2" dependencies = [ "anyhow", "impl-serde 0.5.0", @@ -2009,7 +2009,7 @@ dependencies = [ [[package]] name = "contract-transcode" -version = "5.0.1" +version = "5.0.2" dependencies = [ "anyhow", "assert_matches", diff --git a/crates/analyze/Cargo.toml b/crates/analyze/Cargo.toml index c31ed209e..e443f1dd1 100644 --- a/crates/analyze/Cargo.toml +++ b/crates/analyze/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "contract-analyze" -version = "5.0.1" +version = "5.0.2" authors = ["Use Ink "] edition = "2021" @@ -14,7 +14,7 @@ keywords = ["wasm", "ink", "webassembly", "blockchain", "edsl"] include = ["Cargo.toml", "*.rs", "LICENSE"] [dependencies] -contract-metadata = { version = "5.0.1", path = "../metadata" } +contract-metadata = { version = "5.0.2", path = "../metadata" } wasmparser = "0.220.0" anyhow = "1.0.94" diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml index e31feb74a..65f03e173 100644 --- a/crates/build/Cargo.toml +++ b/crates/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "contract-build" -version = "5.0.1" +version = "5.0.2" authors = ["Use Ink "] edition = "2021" @@ -49,7 +49,7 @@ crossterm = "0.28.1" polkavm-linker = "0.17.1" -contract-metadata = { version = "5.0.1", path = "../metadata" } +contract-metadata = { version = "5.0.2", path = "../metadata" } [target.'cfg(unix)'.dependencies] uzers = "0.12" diff --git a/crates/cargo-contract/Cargo.toml b/crates/cargo-contract/Cargo.toml index 35490cbfc..4f11e98ed 100644 --- a/crates/cargo-contract/Cargo.toml +++ b/crates/cargo-contract/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-contract" -version = "5.0.1" +version = "5.0.2" authors = ["Use Ink "] build = "build.rs" edition = "2021" @@ -18,11 +18,11 @@ include = [ ] [dependencies] -contract-build = { version = "5.0.1", path = "../build" } -contract-extrinsics = { version = "5.0.1", path = "../extrinsics" } -contract-transcode = { version = "5.0.1", path = "../transcode" } -contract-metadata = { version = "5.0.1", path = "../metadata" } -contract-analyze = { version = "5.0.1", path = "../analyze" } +contract-build = { version = "5.0.2", path = "../build" } +contract-extrinsics = { version = "5.0.2", path = "../extrinsics" } +contract-transcode = { version = "5.0.2", path = "../transcode" } +contract-metadata = { version = "5.0.2", path = "../metadata" } +contract-analyze = { version = "5.0.2", path = "../analyze" } anyhow = "1.0.94" clap = { version = "4.5.23", features = ["derive", "env"] } diff --git a/crates/cargo-contract/src/cmd/call.rs b/crates/cargo-contract/src/cmd/call.rs index f0517ad0c..6314f5fe5 100644 --- a/crates/cargo-contract/src/cmd/call.rs +++ b/crates/cargo-contract/src/cmd/call.rs @@ -130,8 +130,8 @@ impl CallCommand { { let contract = parse_account(&self.contract) .map_err(|e| anyhow::anyhow!("Failed to parse contract option: {}", e))?; - let signer = C::Signer::from_str(&self.extrinsic_cli_opts.suri) - .map_err(|_| anyhow::anyhow!("Failed to parse suri option"))?; + + let signer: Result = self.extrinsic_cli_opts.signer(self.extrinsic_cli_opts); let chain = self.extrinsic_cli_opts.chain_cli_opts.chain(); let token_metadata = TokenMetadata::query::(&chain.url()).await?; let storage_deposit_limit = self diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 9378295d0..b3151ea99 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -59,6 +59,10 @@ use crate::{ PathBuf, Weight, }; +use super::{ + config::SignerConfig, +}; +use ink_env::Environment; use anyhow::{ Context, Result, @@ -88,9 +92,14 @@ use std::{ }, str::FromStr, }; +use subxt::{ + Config, +}; /// Arguments required for creating and sending an extrinsic to a Substrate node. -#[derive(Clone, Debug, clap::Args)] +#[derive(Clone, Debug, clap::Parser)] +#[clap(group = clap::ArgGroup::new("surigroup").multiple(false))] +#[clap(group = clap::ArgGroup::new("passgroup").multiple(false))] pub struct CLIExtrinsicOpts { /// Path to a contract build artifact file: a raw `.wasm` file, a `.contract` bundle, /// or a `.json` metadata file. @@ -99,13 +108,23 @@ pub struct CLIExtrinsicOpts { /// Path to the `Cargo.toml` of the contract. #[clap(long, value_parser)] manifest_path: Option, - /// Secret key URI for the account deploying the contract. + /// Secret key URI for the account interacting with the contract. /// /// e.g. /// - for a dev account "//Alice" /// - with a password "//Alice///SECRET_PASSWORD" - #[clap(name = "suri", long, short)] - suri: String, + /// Secret key URI for the account interacting with the contract. + #[clap(group = "surigroup", required = false, name = "suri", long, short('s'), conflicts_with = "suri-path")] + suri: Option, + /// Path to a file containing the secret key URI for the account interacting with the contract. + #[clap(group = "surigroup", required = false, name = "suri-path", long, short('S'), conflicts_with = "suri")] + suri_path: Option, + /// Password for the secret key URI. + #[clap(group = "passgroup", required = false, name = "password", long, short('p'), conflicts_with = "password-path")] + password: Option, + /// Path to a file containing the password for the secret key URI. + #[clap(group = "passgroup", required = false, name = "password-path", long, short('P'), conflicts_with = "password")] + password_path: Option, #[clap(flatten)] verbosity: VerbosityFlags, /// Submit the extrinsic for on-chain execution. @@ -127,6 +146,115 @@ pub struct CLIExtrinsicOpts { } impl CLIExtrinsicOpts { + /// Load suri and password data. + pub fn suri_data_from_raw(&self) -> Result { + SuriData::from_suri_and_password( + self.suri.as_ref(), + self.password.as_ref(), + ) + } + + /// Load suri and password data from files. + pub fn suri_data_from_files(&self) -> Result { + SuriData::from_suri_and_password_files( + self.suri_path.as_ref(), + self.password_path.as_ref(), + ) + } + + // how to return change return type `>::Signer` instead of `sp_core::sr25519::Pair` + pub fn signer>(&self, extrinsic_cli_opts: CLIExtrinsicOpts) -> Result { + // initialise signer + let mut signer = C::Signer::from_str(""); + // if suri and no password, then just load using `from_str` + // if suri and password, then `signer_from_raw` + // if suri_path and password_path, then load signer from `signer_from_files` + // note: cannot provide suri and password_path, or suri_path and password. + match &extrinsic_cli_opts.suri { + Some(s) => { + match &extrinsic_cli_opts.password { + Some(p) => { + // TODO - how to convert `sp_core::sr25519::Pair` into `>::Signer`? + signer = &extrinsic_cli_opts.signer_from_raw(); + }, + None => { + signer = match C::Signer::from_str(&s) { + Ok(s) => Ok(s), + Err(err) => Err(err), + }; + }, + } + }, + None => { + match &extrinsic_cli_opts.suri_path { + Some(sp) => { + match &extrinsic_cli_opts.password_path { + Some(p) => { + // TODO - how to convert `sp_core::sr25519::Pair` into `>::Signer`? + signer = &extrinsic_cli_opts.signer_from_files(); + }, + None => { + return Err(ErrorVariant::Generic(contract_extrinsics::GenericError::from_message(format!("Failed to provide password_path required by suri_path")))); + }, + } + }, + None => { + return Err(ErrorVariant::Generic(contract_extrinsics::GenericError::from_message(format!("Failed to provide required suri or suri_path")))); + }, + } + }, + }; + } + + pub fn signer_from_raw(&self) -> Result { + match &self.suri_data_from_raw() { + // TODO - replace with `Ok` from `anyhow` throughout if possible + // instead of using `std::result::Result` + std::result::Result::Ok(d) => { + // get suri + let suri = match d.suri.suri() { + // remove newline characters + Ok(s) => s.trim().to_string(), + Err(e) => anyhow::bail!("suri not provided"), + }; + // get password + let password = match d.password.password() { + // remove newline characters + Ok(p) => Some(p.trim().to_string()), + Err(e) => anyhow::bail!("password not provided"), + }; + return sp_core::Pair::from_string(&suri, password.as_ref().map(String::as_ref)) + .map_err(|_| anyhow::anyhow!("Secret string error")) + }, + std::result::Result::Err(_e) => anyhow::bail!("suri data not provided. {}", _e), + }; + } + + /// Returns the signer from paths for contract extrinsics. + pub fn signer_from_files(&self) -> Result { + match &self.suri_data_from_files() { + // TODO - replace with `Ok` from `anyhow` throughout if possible + // instead of using `std::result::Result` + std::result::Result::Ok(d) => { + // get suri + let suri = match d.suri.suri() { + // remove newline characters + Ok(s) => s.trim().to_string(), + Err(e) => anyhow::bail!("suri not provided"), + }; + // get password + let password = match d.password.password() { + // remove newline characters + Ok(p) => Some(p.trim().to_string()), + Err(e) => anyhow::bail!("password not provided"), + }; + return sp_core::Pair::from_string(&suri, password.as_ref().map(String::as_ref)) + .map_err(|_| anyhow::anyhow!("Secret string error")) + }, + std::result::Result::Err(_e) => anyhow::bail!("suri data not provided. {}", _e), + }; + } + /// Returns the verbosity pub fn verbosity(&self) -> Result { TryFrom::try_from(&self.verbosity) @@ -193,6 +321,218 @@ impl Chain { } } + +/// The Suri of a contract. +#[derive(Debug)] +pub struct Suri(Option); + +impl Suri { + /// The suri of the contract + pub fn suri(&self) -> Result<&String> { + match &self.0 { + Some(s) => return Ok(s), + None => anyhow::bail!("suri not available"), + } + } +} + +/// The Password of a contract. +#[derive(Debug)] +pub struct Password(Option); + +impl Password { + /// The password of the contract + pub fn password(&self) -> Result<&String> { + match &self.0 { + Some(p) => return Ok(p), + None => anyhow::bail!("password not available"), + } + } +} + +/// Suri and password data for use with extrinsic commands. +#[derive(Debug)] +pub struct SuriData { + /// The expected path of the file containing the suri data path. + suri_path: Option, + /// The expected path of the file containing the password data path. + password_path: Option, + /// The suri if data file exists. + suri: Suri, + /// The password if data file exists. + password: Password, +} + +impl SuriData { + /// Given an suri and an associated password, + /// load the metadata where possible. + pub fn from_suri_and_password( + suri: Option<&String>, + password: Option<&String>, + ) -> Result { + let mut _suri = Suri(Some("".to_string())); + let mut _password = Password(Some("".to_string())); + match suri { + Some(s) => { + tracing::debug!("Reading suri"); + _suri = Suri(Some(s.to_string())); + }, + None => { + anyhow::bail!( + "Failed to load suri" + ); + }, + } + + match password { + Some(p) => { + tracing::debug!("Reading password"); + _password = Password(Some(p.to_string())); + }, + None => { + anyhow::bail!( + "Failed to load password" + ); + }, + } + + Ok(Self { + // TODO - figure out how to avoid using `cloned()` + suri_path: None, + password_path: None, + suri: _suri, + password: _password, + }) + } + + /// Given an suri path and an associated password path, + /// load the metadata where possible. + pub fn from_suri_and_password_files( + suri_path: Option<&PathBuf>, + password_path: Option<&PathBuf>, + ) -> Result { + let mut suri = Suri(None); + let mut password = Password(None); + + match suri_path { + Some(sp) => { + match sp.extension().and_then(|ext| ext.to_str()) { + Some("txt") => { + tracing::debug!("Loading suri path from `{}`", sp.display()); + let file_name = sp.file_stem() + .context("suri file has unreadable name")? + .to_str() + .context("Error parsing filename string")?; + // TODO - consider storing `Vec` in Suri struct instead of `String` + let _s: String = match String::from_utf8(std::fs::read(sp)?) { + std::result::Result::Ok(s) => s, + std::result::Result::Err(_e) => { + anyhow::bail!( + "unable to convert Vec to String for suri_path. {}", _e + ) + } + }; + let s = Suri(Some(_s)); + let dir = sp.parent().map_or_else(PathBuf::new, PathBuf::from); + let metadata_path = dir.join(format!("{file_name}.txt")); + if metadata_path.exists() { + suri = match s.suri() { + std::result::Result::Ok(s) => Suri(Some(s.to_string())), + std::result::Result::Err(_e) => { + anyhow::bail!("unable to get value for Suri. {}", _e) + } + }; + } else { + anyhow::bail!("suri file does not exist") + } + suri = match s.suri() { + std::result::Result::Ok(s) => Suri(Some(s.to_string())), + std::result::Result::Err(_e) => { + anyhow::bail!("unable to get value for Suri. {}", _e) + } + }; + } + Some(ext) => anyhow::bail!( + "Invalid extension {ext}, expected `.txt`" + ), + None => { + anyhow::bail!( + "suri path has no extension, expected `.txt`" + ) + } + }; + }, + None => { + anyhow::bail!( + "suri path has no extension, expected `.txt`" + ) + } + } + + match password_path { + Some(pp) => { + match pp.extension().and_then(|ext| ext.to_str()) { + Some("txt") => { + tracing::debug!("Loading password path from `{}`", pp.display()); + let file_name = pp.file_stem() + .context("password file has unreadable name")? + .to_str() + .context("Error parsing filename string")?; + let _p: String = match String::from_utf8(std::fs::read(pp)?) { + std::result::Result::Ok(p) => p, + std::result::Result::Err(_e) => { + anyhow::bail!( + "unable to convert Vec to String for password_path. {}", _e + ) + } + }; + let p = Password(Some(_p)); + let dir = pp.parent().map_or_else(PathBuf::new, PathBuf::from); + let metadata_path = dir.join(format!("{file_name}.txt")); + if metadata_path.exists() { + password = match p.password() { + std::result::Result::Ok(p) => Password(Some(p.to_string())), + std::result::Result::Err(_e) => { + anyhow::bail!("unable to get value for Password. {}", _e) + } + }; + } else { + anyhow::bail!("password file does not exist") + } + password = match p.password() { + std::result::Result::Ok(p) => Password(Some(p.to_string())), + std::result::Result::Err(_e) => { + anyhow::bail!("unable to get value for Password. {}", _e) + } + }; + } + Some(ext) => anyhow::bail!( + "Invalid extension {ext}, expected `.txt`" + ), + None => { + anyhow::bail!( + "password path has no extension, expected `.txt`" + ) + } + }; + }, + None => { + anyhow::bail!( + "password path has no extension, expected `.txt`" + ) + } + } + + Ok(Self { + // TODO - figure out how to avoid using `cloned()` + suri_path: suri_path.cloned(), + password_path: password_path.cloned(), + suri: suri, + password: password, + }) + } +} + const STORAGE_DEPOSIT_KEY: &str = "Storage Total Deposit"; pub const MAX_KEY_COL_WIDTH: usize = STORAGE_DEPOSIT_KEY.len() + 1; diff --git a/crates/extrinsics/Cargo.toml b/crates/extrinsics/Cargo.toml index 77e68913f..11a52e47b 100644 --- a/crates/extrinsics/Cargo.toml +++ b/crates/extrinsics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "contract-extrinsics" -version = "5.0.1" +version = "5.0.2" authors = ["Use Ink "] edition = "2021" rust-version = "1.70" @@ -15,9 +15,9 @@ keywords = ["wasm", "ink", "webassembly", "blockchain", "edsl"] include = ["Cargo.toml", "*.rs", "LICENSE",] [dependencies] -contract-build = { version = "5.0.1", path = "../build" } -contract-metadata = { version = "5.0.1", path = "../metadata" } -contract-transcode = { version = "5.0.1", path = "../transcode" } +contract-build = { version = "5.0.2", path = "../build" } +contract-metadata = { version = "5.0.2", path = "../metadata" } +contract-transcode = { version = "5.0.2", path = "../transcode" } anyhow = "1.0.94" blake2 = { version = "0.10.6", default-features = false } diff --git a/crates/extrinsics/README.md b/crates/extrinsics/README.md index c88f5d8df..9bc3f038f 100644 --- a/crates/extrinsics/README.md +++ b/crates/extrinsics/README.md @@ -23,11 +23,21 @@ visible on screen and are often saved to the command line shell's history. For n development and testnets. It is a priority to implement a safer method of signing here before using this tool with value bearing chains. +``` +--suri-path +``` +*Optional*. The path to a file containing the secret key URI for the account interacting with the contract. + ``` --password ``` *Optional*. The password for the `--suri`, see https://docs.substrate.io/reference/command-line-tools/subkey/#password-protected-keys. +``` +--password-path +``` +*Optional*. The path to a file containing the password for the secret key URI. + ``` --manifest-path ``` @@ -60,6 +70,15 @@ dispatchable. e.g. `cargo contract upload --suri //Alice` +e.g. +``` +cargo contract upload \ + --suri-path ../path/to/my-secret-key-uri-file.txt \ + --password-path ../path/to/my-secret-key-uri-password-file.txt +``` +- `--suri-path` the path to a file containing the secret key URI for the account interacting with the contract. +- `--password-path` the path to a file containing the password for the secret key URI. + Assumes that `cargo contract build` has already been run to produce the contract artifacts. ### `instantiate` diff --git a/crates/extrinsics/src/integration_tests.rs b/crates/extrinsics/src/integration_tests.rs index 5fca264eb..5b1d50929 100644 --- a/crates/extrinsics/src/integration_tests.rs +++ b/crates/extrinsics/src/integration_tests.rs @@ -212,6 +212,8 @@ async fn build_upload_instantiate_call() { let stderr = str::from_utf8(&output.stderr).unwrap(); assert!(output.status.success(), "upload code failed: {stderr}"); + // TODO - add tests for `suri-path` and `password-path` + let output = cargo_contract(project_path.as_path()) .arg("instantiate") .args(["--constructor", "new"]) diff --git a/crates/metadata/Cargo.toml b/crates/metadata/Cargo.toml index 7c4619b73..67129ee8f 100644 --- a/crates/metadata/Cargo.toml +++ b/crates/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "contract-metadata" -version = "5.0.1" +version = "5.0.2" authors = ["Use Ink "] edition = "2021" diff --git a/crates/metadata/compatibility_list.json b/crates/metadata/compatibility_list.json index 36f94bfda..afc91523c 100644 --- a/crates/metadata/compatibility_list.json +++ b/crates/metadata/compatibility_list.json @@ -28,11 +28,23 @@ "5.0.1" ] }, + "4.1.2": { + "ink": [ + ">=5.0.0-rc.2", + "5.0.0" + ] + }, "5.0.1": { "ink": [ ">=5.0.0-rc.2", "5.0.1" ] + }, + "5.0.2": { + "ink": [ + ">=5.0.0-rc.2", + "5.0.1" + ] } } } diff --git a/crates/transcode/Cargo.toml b/crates/transcode/Cargo.toml index 9226541c0..328c2155a 100644 --- a/crates/transcode/Cargo.toml +++ b/crates/transcode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "contract-transcode" -version = "5.0.1" +version = "5.0.2" authors = ["Use Ink "] edition = "2021" @@ -20,7 +20,7 @@ path = "src/lib.rs" anyhow = "1.0.94" base58 = { version = "0.2.0" } blake2 = { version = "0.10.6", default-features = false } -contract-metadata = { version = "5.0.1", path = "../metadata" } +contract-metadata = { version = "5.0.2", path = "../metadata" } escape8259 = "0.5.2" hex = "0.4.3" indexmap = "2.2.6"