diff --git a/Cargo.toml b/Cargo.toml index 54ef0b71..a8ff1901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,11 @@ [package] name = "casper-client" version = "2.0.0" # when updating, also update 'html_root_url' in lib.rs -authors = ["Marc Brinkmann ", "Fraser Hutchison ", "Zachary Showalter "] +authors = [ + "Marc Brinkmann ", + "Fraser Hutchison ", + "Zachary Showalter ", +] edition = "2021" description = "A client library and binary for interacting with the Casper network" documentation = "https://docs.rs/casper-client" @@ -19,19 +23,22 @@ path = "lib/lib.rs" name = "casper-client" path = "src/main.rs" doc = false -required-features = ["async-trait", "clap", "clap_complete", "tokio", "std-fs-io"] +required-features = ["async-trait", "clap", "clap_complete", "std-fs-io"] [features] -default = ["async-trait", "clap", "clap_complete", "tokio", "std-fs-io"] +default = ["async-trait", "clap", "clap_complete", "std-fs-io"] std-fs-io = ["casper-types/std-fs-io"] [dependencies] async-trait = { version = "0.1.74", optional = true } base16 = "0.2.1" +base64 = { version = "0.22.1", default-features = false } +bytes = { version = "1.6.0", default-features = false } casper-hashing = "3.0.0" casper-types = { version = "4.0.1", features = ["std"] } -clap = { version = "~4.4", optional = true, features = ["cargo", "deprecated", "wrap_help"] } +clap = { version = "~4.4", optional = true, features = ["cargo", "deprecated"] } clap_complete = { version = "<4.5.0", optional = true } +flate2 = "1.0.30" hex-buffer-serde = "0.4.0" humantime = "2.1.0" itertools = "0.12.0" @@ -39,12 +46,13 @@ jsonrpc-lite = "0.6.0" num-traits = "0.2.15" once_cell = "1.18.0" rand = "0.8.5" -reqwest = { version = "0.12.3", features = ["json"] } +reqwest = { version = "0.12.4", features = ["json"] } schemars = "=0.8.5" serde = { version = "1.0.193", default-features = false, features = ["derive"] } serde_json = { version = "1.0.108", features = ["preserve_order"] } -thiserror = "1.0.50" -tokio = { version = "1.34.0", optional = true, features = ["macros", "rt", "sync", "time"] } +tar = { version = "0.4.41", default-features = false } +thiserror = "1" +tokio = { version = "1.38.0", features = ["macros", "rt", "sync", "time"] } uint = "0.9.5" [dev-dependencies] diff --git a/lib/cli.rs b/lib/cli.rs index 19102854..2c72af95 100644 --- a/lib/cli.rs +++ b/lib/cli.rs @@ -53,6 +53,7 @@ use crate::{ DictionaryItemIdentifier, }, types::Deploy, + verification_types::VerificationDetails, SuccessResponse, }; #[cfg(doc)] @@ -708,3 +709,22 @@ pub async fn get_era_info( .await .map_err(CliError::from) } + +/// Verifies the smart contract code againt the one deployed at address. +pub async fn verify_contract( + hash_str: &str, + verification_url_base_path: &str, + verification_project_path: Option<&str>, + verbosity_level: u64, +) -> Result { + let key = parse::key_for_query(hash_str)?; + let verbosity = parse::verbosity(verbosity_level); + crate::verify_contract( + key, + verification_url_base_path, + verification_project_path, + verbosity, + ) + .await + .map_err(CliError::from) +} diff --git a/lib/cli/json_args.rs b/lib/cli/json_args.rs index 00c88282..2ba4e1aa 100644 --- a/lib/cli/json_args.rs +++ b/lib/cli/json_args.rs @@ -59,59 +59,59 @@ fn write_json_to_bytesrepr( .as_i64() .and_then(|value| i32::try_from(value).ok()) .ok_or(ErrorDetails::CannotParseToI32)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::I64, Value::Number(number)) => { let value = number.as_i64().ok_or(ErrorDetails::CannotParseToI64)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::U8, Value::Number(number)) => { let value = number .as_u64() .and_then(|value| u8::try_from(value).ok()) .ok_or(ErrorDetails::CannotParseToU8)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::U32, Value::Number(number)) => { let value = number .as_u64() .and_then(|value| u32::try_from(value).ok()) .ok_or(ErrorDetails::CannotParseToU32)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::U64, Value::Number(number)) => { let value = number.as_u64().ok_or(ErrorDetails::CannotParseToU64)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::U128, Value::String(string)) => { let value = U128::from_dec_str(string)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::U128, Value::Number(number)) => { let value = number.as_u64().ok_or(ErrorDetails::CannotParseToU64)?; - U128::from(value).write_bytes(output)? + U128::from(value).write_bytes(output)?; } (&CLType::U256, Value::String(string)) => { let value = U256::from_dec_str(string)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::U256, Value::Number(number)) => { let value = number.as_u64().ok_or(ErrorDetails::CannotParseToU64)?; - U256::from(value).write_bytes(output)? + U256::from(value).write_bytes(output)?; } (&CLType::U512, Value::String(string)) => { let value = U512::from_dec_str(string)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::U512, Value::Number(number)) => { let value = number.as_u64().ok_or(ErrorDetails::CannotParseToU64)?; - U512::from(value).write_bytes(output)? + U512::from(value).write_bytes(output)?; } (&CLType::Unit, Value::Null) => (), (&CLType::String, Value::String(string)) => string.write_bytes(output)?, (&CLType::Key, Value::String(string)) => { let value = Key::from_formatted_str(string)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::Key, Value::Object(map)) => { // This is an alternative JSON representation of a `Key`, e.g. if calling @@ -141,22 +141,22 @@ fn write_json_to_bytesrepr( Key::ChainspecRegistry if mapped_variant == "ChainspecRegistry" => {} _ => return Err(ErrorDetails::KeyObjectHasInvalidVariant), } - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::URef, Value::String(string)) => { let value = URef::from_formatted_str(string)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (&CLType::PublicKey, Value::String(string)) => { let value = PublicKey::from_hex(string)?; - value.write_bytes(output)? + value.write_bytes(output)?; } (CLType::Option(ref _inner_cl_type), Value::Null) => { output.push(OPTION_NONE_TAG); } (CLType::Option(ref inner_cl_type), _) => { output.push(OPTION_SOME_TAG); - write_json_to_bytesrepr(inner_cl_type, json_value, output)? + write_json_to_bytesrepr(inner_cl_type, json_value, output)?; } (CLType::List(ref inner_cl_type), Value::Array(vec)) => { (vec.len() as u32).write_bytes(output)?; @@ -209,11 +209,11 @@ fn write_json_to_bytesrepr( match map.iter().next() { Some((key, value)) if key.to_ascii_lowercase() == "ok" => { output.push(RESULT_OK_TAG); - write_json_to_bytesrepr(ok, value, output)? + write_json_to_bytesrepr(ok, value, output)?; } Some((key, value)) if key.to_ascii_lowercase() == "err" => { output.push(RESULT_ERR_TAG); - write_json_to_bytesrepr(err, value, output)? + write_json_to_bytesrepr(err, value, output)?; } _ => return Err(ErrorDetails::ResultObjectHasInvalidVariant), } @@ -243,7 +243,7 @@ fn write_json_to_bytesrepr( _ => return Err(ErrorDetails::MapTypeNotValidAsObject(*key_type.clone())), }; (map.len() as u32).write_bytes(output)?; - for (key_as_str, value) in map.iter() { + for (key_as_str, value) in map { let key = match **key_type { CLType::I32 => json!(i32::from_str(key_as_str)?), CLType::I64 => json!(i64::from_str(key_as_str)?), @@ -294,7 +294,7 @@ fn write_json_to_bytesrepr( actual: vec.len(), }); } - write_json_to_bytesrepr(&inner_cl_types[0], &vec[0], output)? + write_json_to_bytesrepr(&inner_cl_types[0], &vec[0], output)?; } (CLType::Tuple2(ref inner_cl_types), Value::Array(vec)) => { if vec.len() != inner_cl_types.len() { @@ -304,7 +304,7 @@ fn write_json_to_bytesrepr( }); } write_json_to_bytesrepr(&inner_cl_types[0], &vec[0], output)?; - write_json_to_bytesrepr(&inner_cl_types[1], &vec[1], output)? + write_json_to_bytesrepr(&inner_cl_types[1], &vec[1], output)?; } (CLType::Tuple3(ref inner_cl_types), Value::Array(vec)) => { if vec.len() != inner_cl_types.len() { @@ -315,7 +315,7 @@ fn write_json_to_bytesrepr( } write_json_to_bytesrepr(&inner_cl_types[0], &vec[0], output)?; write_json_to_bytesrepr(&inner_cl_types[1], &vec[1], output)?; - write_json_to_bytesrepr(&inner_cl_types[2], &vec[2], output)? + write_json_to_bytesrepr(&inner_cl_types[2], &vec[2], output)?; } _ => return Err(ErrorDetails::IncompatibleType), }; diff --git a/lib/error.rs b/lib/error.rs index a0970b32..875b0c67 100644 --- a/lib/error.rs +++ b/lib/error.rs @@ -172,6 +172,14 @@ pub enum Error { /// Failed to validate response. #[error("invalid response: {0}")] ResponseFailedValidation(#[from] ValidateResponseError), + + /// Failed to verify contract. + #[error("contract verification failed")] + ContractVerificationFailed, + + /// Failed to construct HTTP client. + #[error("failed to construct HTTP client")] + FailedToConstructHttpClient, } impl From for Error { diff --git a/lib/lib.rs b/lib/lib.rs index 94e83cf4..d6b33549 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -48,12 +48,14 @@ mod transfer_target; pub mod types; mod validation; mod verbosity; +mod verification; +mod verification_types; +use std::{env::current_dir, path::Path}; #[cfg(feature = "std-fs-io")] use std::{ fs, io::{Cursor, Read, Write}, - path::Path, }; #[cfg(feature = "std-fs-io")] @@ -111,6 +113,10 @@ use types::{Account, Block, StoredValue}; use types::{Deploy, DeployHash}; pub use validation::ValidateResponseError; pub use verbosity::Verbosity; +pub use verification::{build_archive, send_verification_request}; +use verification_types::VerificationDetails; + +use base64::{engine::general_purpose::STANDARD, Engine}; /// Puts a [`Deploy`] to the network for execution. /// @@ -554,3 +560,48 @@ pub async fn get_era_info( .send_request(GET_ERA_INFO_METHOD, params) .await } + +/// Verifies the smart contract code againt the one deployed at deploy hash. +pub async fn verify_contract( + key: Key, + verification_url_base_path: &str, + project_path: Option<&str>, + verbosity: Verbosity, +) -> Result { + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Key: {key}"); + println!("Verification service base path: {verification_url_base_path}",); + } + + let project_path = match project_path { + Some(path) => Path::new(path).to_path_buf(), + None => match current_dir() { + Ok(path) => path, + Err(error) => { + eprintln!("Cannot get current directory: {error}"); + return Err(Error::ContractVerificationFailed); + } + }, + }; + + let archive = match build_archive(&project_path) { + Ok(archive) => { + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Created project archive (size: {})", archive.len()); + } + archive + } + Err(error) => { + eprintln!("Cannot create project archive: {error}"); + return Err(Error::ContractVerificationFailed); + } + }; + + send_verification_request( + key, + verification_url_base_path, + STANDARD.encode(&archive), + verbosity, + ) + .await +} diff --git a/lib/rpcs/v1_6_0.rs b/lib/rpcs/v1_6_0.rs index 1ea56b41..10107019 100644 --- a/lib/rpcs/v1_6_0.rs +++ b/lib/rpcs/v1_6_0.rs @@ -70,9 +70,7 @@ pub(crate) mod get_block_transfers { } pub(crate) mod get_dictionary_item { - pub use crate::rpcs::v1_5_0::get_dictionary_item::{ - DictionaryItemIdentifier, GetDictionaryItemResult, - }; + pub use crate::rpcs::v1_5_0::get_dictionary_item::GetDictionaryItemResult; pub(crate) use crate::rpcs::v1_5_0::get_dictionary_item::{ GetDictionaryItemParams, GET_DICTIONARY_ITEM_METHOD, }; diff --git a/lib/verification.rs b/lib/verification.rs new file mode 100644 index 00000000..b6d25808 --- /dev/null +++ b/lib/verification.rs @@ -0,0 +1,219 @@ +use std::{cmp::min, io, path::Path}; + +use bytes::{BufMut, Bytes, BytesMut}; +use casper_types::Key; +use flate2::{write::GzEncoder, Compression}; +use reqwest::{ + header::{HeaderMap, HeaderValue, CONTENT_TYPE}, + Client, ClientBuilder, StatusCode, +}; +use tar::Builder as TarBuilder; +use tokio::time::{sleep, Duration}; + +use crate::{ + verification_types::{ + VerificationDetails, VerificationRequest, VerificationResult, VerificationStatus, + }, + Error, Verbosity, +}; + +const MAX_RETRIES: u32 = 10; +const BASE_DELAY: Duration = Duration::from_secs(3); +const MAX_DELAY: Duration = Duration::from_secs(300); + +static GIT_DIR_NAME: &str = ".git"; +static TARGET_DIR_NAME: &str = "target"; + +/// Builds an archive from the specified path. +/// +/// This function creates a compressed tar archive from the files and directories located at the +/// specified path. It excludes the `.git` and `target` directories from the archive. +/// +/// # Arguments +/// +/// * `path` - The path to the directory containing the files and directories to be archived. +/// +/// # Returns +/// +/// The compressed tar archive as a `Bytes` object, or an `std::io::Error` if an error occurs during +/// the archiving process. +pub fn build_archive(path: &Path) -> Result { + let buffer = BytesMut::new().writer(); + let encoder = GzEncoder::new(buffer, Compression::best()); + let mut archive = TarBuilder::new(encoder); + + for entry in path.read_dir()?.flatten() { + let file_name = entry.file_name(); + // Skip `.git` and `target`. + if file_name == TARGET_DIR_NAME || file_name == GIT_DIR_NAME { + continue; + } + let full_path = entry.path(); + if full_path.is_dir() { + archive.append_dir_all(&file_name, &full_path)?; + } else { + archive.append_path_with_name(&full_path, &file_name)?; + } + } + + let encoder = archive.into_inner()?; + let buffer = encoder.finish()?; + Ok(buffer.into_inner().freeze()) +} + +/// Verifies the smart contract code against the one deployed at deploy hash. +/// +/// Sends a verification request to the specified verification URL base path, including the deploy hash, +/// public key, and code archive. +/// +/// # Arguments +/// +/// * `key` - The key of the deployed contract. +/// * `base_url` - The base path of the verification URL. +/// * `code_archive` - Base64-encoded tar-gzipped archive of the source code. +/// * `verbosity` - The verbosity level of the verification process. +/// +/// # Returns +/// +/// The verification details of the contract. +pub async fn send_verification_request( + key: Key, + base_url: &str, + code_archive: String, + verbosity: Verbosity, +) -> Result { + let verification_request = VerificationRequest { + deploy_hash: key, + code_archive, + }; + + fn make_client() -> reqwest::Result { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + let builder = ClientBuilder::new() + .default_headers(headers) + .user_agent("casper-client-rs"); + + // https://github.com/hyperium/hyper/issues/2136 + #[cfg(not(target_arch = "wasm32"))] + let builder = builder.pool_max_idle_per_host(0); + + builder.build() + } + + let Ok(http_client) = make_client() else { + eprintln!("Failed to build HTTP client"); + return Err(Error::FailedToConstructHttpClient); + }; + + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Sending verification request"); + } + + let url = base_url.to_string() + "/verification"; + let response = match http_client + .post(url) + .json(&verification_request) + .send() + .await + { + Ok(response) => response, + Err(error) => { + eprintln!("Cannot send verification request: {error:?}"); + return Err(Error::ContractVerificationFailed); + } + }; + + match response.status() { + StatusCode::OK => { + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Sent verification request",); + } + } + status => { + eprintln!("Verification failed with status {status}"); + } + } + + wait_for_verification_finished(base_url, &http_client, key, verbosity).await; + + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Getting verification details..."); + } + + let url = base_url.to_string() + "/verification/" + &key.to_formatted_string() + "/details"; + match http_client.get(url).send().await { + Ok(response) => response.json().await.map_err(|err| { + eprintln!("Failed to parse JSON {err}"); + Error::ContractVerificationFailed + }), + Err(error) => { + eprintln!("Cannot get verification details: {error:?}"); + Err(Error::ContractVerificationFailed) + } + } +} + +/// Waits for the verification process to finish. +async fn wait_for_verification_finished( + base_url: &str, + http_client: &Client, + key: Key, + verbosity: Verbosity, +) { + let mut retries = MAX_RETRIES; + let mut delay = BASE_DELAY; + + while retries != 0 { + sleep(delay).await; + + match get_verification_status(base_url, http_client, key).await { + Ok(status) => { + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Verification status: {status:?}"); + } + if status == VerificationStatus::Verified || status == VerificationStatus::Failed { + break; + } + } + Err(error) => { + eprintln!("Cannot get verification status: {error:?}"); + break; + } + }; + + retries -= 1; + delay = min(delay * 2, MAX_DELAY); + } +} + +/// Gets the verification status of the contract. +async fn get_verification_status( + base_url: &str, + http_client: &Client, + key: Key, +) -> Result { + let url = base_url.to_string() + "/verification/" + &key.to_formatted_string() + "/status"; + let response = match http_client.get(url).send().await { + Ok(response) => response, + Err(error) => { + eprintln!("Failed to fetch verification status: {error:?}"); + return Err(Error::ContractVerificationFailed); + } + }; + + match response.status() { + StatusCode::OK => { + let result: VerificationResult = response.json().await.map_err(|err| { + eprintln!("Failed to parse JSON for verification status, {err}"); + Error::ContractVerificationFailed + })?; + Ok(result.status) + } + status => { + eprintln!("Verification status not found, {status}"); + Err(Error::ContractVerificationFailed) + } + } +} diff --git a/lib/verification_types.rs b/lib/verification_types.rs new file mode 100644 index 00000000..91b14223 --- /dev/null +++ b/lib/verification_types.rs @@ -0,0 +1,60 @@ +use casper_types::Key; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub(crate) enum VerificationStatus { + Failed, + Pending, + Verified, + Waiting, +} + +// Any update to this enum should be reflected in migrations. +#[derive(Deserialize, Serialize)] +#[non_exhaustive] +pub(crate) enum VerificationErrorCode { + Ok, + None, + BytecodeMismatch, + BuildError, + ContractAlreadyVerified, + ContractNotFound, + Internal, + InvalidContract, + InvalidHash, + WrongCodeArchive, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct VerificationError { + pub(crate) code: VerificationErrorCode, + pub(crate) description: String, +} + +#[derive(Serialize)] +pub(crate) struct VerificationRequest { + // Deploy hash of the contract deployment transaction. + pub(crate) deploy_hash: Key, + // Base64 encoded tar archive containing contract source code. + pub(crate) code_archive: String, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct VerificationResult { + pub(crate) status: VerificationStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) error: Option, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct VerificationDetailsResult { + // pub(crate) toolchain: Toolchain, + pub(crate) binary_uri: String, + pub(crate) logs_uri: String, +} + +#[derive(Deserialize, Serialize)] +pub struct VerificationDetails { + pub(crate) status: VerificationStatus, + pub(crate) result: VerificationDetailsResult, +} diff --git a/src/common.rs b/src/common.rs index 825cc69e..2379388d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -374,3 +374,27 @@ pub(super) mod purse_uref { matches.get_one::(ARG_NAME).map(String::as_str) } } + +/// Handles providing the arg for and retrieval of the deploy hash. +pub mod deploy_hash { + use super::*; + + const ARG_NAME: &str = "deploy-hash"; + const ARG_VALUE_NAME: &str = "HEX STRING"; + const ARG_HELP: &str = "Hex-encoded deploy hash"; + + pub fn arg(display_order: usize) -> Arg { + Arg::new(ARG_NAME) + .required(true) + .value_name(ARG_VALUE_NAME) + .help(ARG_HELP) + .display_order(display_order) + } + + pub fn get(matches: &ArgMatches) -> &str { + matches + .get_one::(ARG_NAME) + .map(String::as_str) + .unwrap_or_else(|| panic!("should have {} arg", ARG_NAME)) + } +} diff --git a/src/deploy/get.rs b/src/deploy/get.rs index fbba91a1..3e35ffa2 100644 --- a/src/deploy/get.rs +++ b/src/deploy/get.rs @@ -18,30 +18,6 @@ enum DisplayOrder { FinalizedApprovals, } -/// Handles providing the arg for and retrieval of the deploy hash. -mod deploy_hash { - use super::*; - - const ARG_NAME: &str = "deploy-hash"; - const ARG_VALUE_NAME: &str = "HEX STRING"; - const ARG_HELP: &str = "Hex-encoded deploy hash"; - - pub(super) fn arg() -> Arg { - Arg::new(ARG_NAME) - .required(true) - .value_name(ARG_VALUE_NAME) - .help(ARG_HELP) - .display_order(DisplayOrder::DeployHash as usize) - } - - pub(super) fn get(matches: &ArgMatches) -> &str { - matches - .get_one::(ARG_NAME) - .map(String::as_str) - .unwrap_or_else(|| panic!("should have {} arg", ARG_NAME)) - } -} - /// Handles providing the arg for the retrieval of the finalized approvals. mod finalized_approvals { use super::*; @@ -85,7 +61,7 @@ impl ClientCommand for GetDeploy { DisplayOrder::NodeAddress as usize, )) .arg(common::rpc_id::arg(DisplayOrder::RpcId as usize)) - .arg(deploy_hash::arg()) + .arg(common::deploy_hash::arg(DisplayOrder::DeployHash as usize)) .arg(finalized_approvals::arg()) } @@ -93,7 +69,7 @@ impl ClientCommand for GetDeploy { let maybe_rpc_id = common::rpc_id::get(matches); let node_address = common::node_address::get(matches); let verbosity_level = common::verbose::get(matches); - let deploy_hash = deploy_hash::get(matches); + let deploy_hash = common::deploy_hash::get(matches); let finalized_approvals = finalized_approvals::get(matches); casper_client::cli::get_deploy( diff --git a/src/main.rs b/src/main.rs index d087797d..32793126 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod keygen; mod list_rpcs; mod query_balance; mod query_global_state; +mod verify_contract; use std::process; @@ -49,12 +50,13 @@ use keygen::Keygen; use list_rpcs::ListRpcs; use query_balance::QueryBalance; use query_global_state::QueryGlobalState; +use verify_contract::VerifyContract; const APP_NAME: &str = "Casper client"; static VERSION: Lazy = Lazy::new( - || match option_env!("GIT_SHA_SHORT").map(|sha| sha.to_lowercase()) { + || match option_env!("GIT_SHA_SHORT").map(str::to_lowercase) { None => crate_version!().to_string(), Some(git_sha_short) => { if git_sha_short.to_lowercase() == "unknown" { @@ -95,6 +97,7 @@ enum DisplayOrder { Keygen, AccountAddress, GenerateCompletion, + VerifyContract, } fn cli() -> Command { @@ -140,6 +143,7 @@ fn cli() -> Command { .subcommand(GenerateCompletion::build( DisplayOrder::GenerateCompletion as usize, )) + .subcommand(VerifyContract::build(DisplayOrder::VerifyContract as usize)) } #[tokio::main(flavor = "current_thread")] @@ -179,6 +183,7 @@ async fn main() { Keygen::NAME => Keygen::run(matches).await, AccountAddress::NAME => AccountAddress::run(matches).await, GenerateCompletion::NAME => GenerateCompletion::run(matches).await, + VerifyContract::NAME => VerifyContract::run(matches).await, _ => { let _ = cli().print_long_help(); println!(); @@ -188,7 +193,7 @@ async fn main() { let mut verbosity_level = common::verbose::get(matches); if verbosity_level == 0 { - verbosity_level += 1 + verbosity_level += 1; } match result { diff --git a/src/verify_contract.rs b/src/verify_contract.rs new file mode 100644 index 00000000..afbe7611 --- /dev/null +++ b/src/verify_contract.rs @@ -0,0 +1,101 @@ +use std::str; + +use async_trait::async_trait; +use casper_client::cli::CliError; +use clap::{ArgMatches, Command}; + +use crate::{command::ClientCommand, common, Success}; + +pub struct VerifyContract; + +/// This struct defines the order in which the args are shown for this subcommand's help message. +enum DisplayOrder { + Verbose, + DeployHash, + VerificationUrlBasePath, + VerificationProjectPath, +} + +mod verification_url_base_path { + use clap::{Arg, ArgMatches}; + + static ARG_NAME: &str = "verification-url-basepath"; + + pub fn arg(order: usize) -> Arg { + Arg::new(ARG_NAME) + .long(ARG_NAME) + .short('u') + .required(false) + .default_value("http://localhost:8080") + .value_name("HOST:PORT") + .help("Hostname or IP and port of the verification API") + .display_order(order) + } + + pub fn get(matches: &ArgMatches) -> &str { + matches + .get_one::(ARG_NAME) + .map(String::as_str) + .unwrap_or_else(|| panic!("should have {ARG_NAME} arg")) + } +} + +mod verification_project_path { + use clap::{Arg, ArgMatches}; + + static ARG_NAME: &str = "verification-source-code-path"; + + pub fn arg(order: usize) -> Arg { + Arg::new(ARG_NAME) + .long(ARG_NAME) + .short('p') + .required(false) + .value_name("PATH") + .help("Source code path") + .display_order(order) + } + + pub fn get(matches: &ArgMatches) -> Option<&str> { + matches.get_one::(ARG_NAME).map(String::as_str) + } +} + +#[async_trait] +impl ClientCommand for VerifyContract { + const NAME: &'static str = "verify-contract"; + const ABOUT: &'static str = + "Verifies a smart contracts source code using verification service. \ + The source code will be uploaded, built, and compared against the deployed contract binary. \ + You may specify a path from which the code will be read and compressed from, or omit the path. \ + If the path is omitted, the archive will be built from the current working directory."; + + fn build(display_order: usize) -> Command { + Command::new(Self::NAME) + .about(Self::ABOUT) + .display_order(display_order) + .arg(common::verbose::arg(DisplayOrder::Verbose as usize)) + .arg(common::deploy_hash::arg(DisplayOrder::DeployHash as usize)) + .arg(verification_url_base_path::arg( + DisplayOrder::VerificationUrlBasePath as usize, + )) + .arg(verification_project_path::arg( + DisplayOrder::VerificationProjectPath as usize, + )) + } + + async fn run(matches: &ArgMatches) -> Result { + let deploy_hash = common::deploy_hash::get(matches); + let verification_url_base_path = verification_url_base_path::get(matches); + let verification_project_path = verification_project_path::get(matches); + let verbosity_level = common::verbose::get(matches); + + casper_client::cli::verify_contract( + deploy_hash, + verification_url_base_path, + verification_project_path, + verbosity_level, + ) + .await + .map(Success::from) + } +}