diff --git a/.gitignore b/.gitignore index 6bd0c854..f3a55b4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /target Cargo.lock *casper_keygen_test* +.idea/ +.envrc +.direnv/ diff --git a/Cargo.toml b/Cargo.toml index 1007f053..5520da9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,9 +32,12 @@ std-fs-io = ["casper-types/std-fs-io"] [dependencies] async-trait = { version = "0.1.74", default-features = false, optional = true } base16 = "0.2.1" +base64 = { version = "0.22.1", default-features = false } +bytes = { version = "1.6.0", default-features = false } casper-types = { version = "5.0.0", features = ["std", "json-schema"] } clap = { version = "~4.4", features = ["cargo", "deprecated"], optional = true } clap_complete = { version = "~4.4", default-features = false, optional = true } +flate2 = "1.0.30" hex-buffer-serde = "0.4.0" humantime = "2.1.0" itertools = "0.12.0" @@ -47,6 +50,7 @@ schemars = "0.8.18" serde = { version = "1", default-features = false, features = ["derive"] } serde-map-to-array = "1.1.1" serde_json = { version = "1", features = ["preserve_order"] } +tar = { version = "0.4.41", default-features = false } thiserror = "1" tokio = { version = "1.39.3", features = ["macros", "rt", "sync", "time"] } uint = "0.9.5" diff --git a/lib/cli.rs b/lib/cli.rs index ee1e1125..8a516a00 100644 --- a/lib/cli.rs +++ b/lib/cli.rs @@ -58,6 +58,9 @@ use crate::{ }, SuccessResponse, }; + +#[cfg(feature = "std-fs-io")] +use crate::verification_types::VerificationDetails; #[cfg(doc)] use crate::{Account, Block, Error, StoredValue, Transfer}; #[cfg(doc)] @@ -614,3 +617,23 @@ pub async fn get_era_info( .await .map_err(CliError::from) } + +/// Verifies the smart contract code against the one installed +/// by deploy or transaction with given hash. +#[cfg(feature = "std-fs-io")] +pub async fn verify_contract( + hash_str: &str, + verification_url_base_path: &str, + verification_project_path: Option<&str>, + verbosity_level: u64, +) -> Result { + let verbosity = parse::verbosity(verbosity_level); + crate::verify_contract( + hash_str, + 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 fa3bab4c..31d5def2 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 5d0cd2bf..7faf9ea2 100644 --- a/lib/error.rs +++ b/lib/error.rs @@ -158,6 +158,14 @@ pub enum Error { /// Underlying error. error: std::str::Utf8Error, }, + + /// 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 20b3d1e0..2e2450f4 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -46,9 +46,12 @@ pub mod rpcs; pub mod types; mod validation; mod verbosity; +mod verification; +mod verification_types; #[cfg(any(feature = "std-fs-io", test))] use std::{ + env::current_dir, fs, io::{Cursor, Read, Write}, path::Path, @@ -65,6 +68,8 @@ use casper_types::{ #[cfg(any(feature = "std-fs-io", test))] use casper_types::{SecretKey, TransactionV1}; +#[cfg(any(feature = "std-fs-io", test))] +use base64::{engine::general_purpose::STANDARD, Engine}; pub use error::Error; use json_rpc::JsonRpcCall; pub use json_rpc::{JsonRpcId, SuccessResponse}; @@ -112,6 +117,9 @@ use rpcs::{ }; pub use validation::ValidateResponseError; pub use verbosity::Verbosity; +pub use verification::{build_archive, send_verification_request}; +#[cfg(any(feature = "std-fs-io", test))] +use verification_types::VerificationDetails; /// The maximum permissible size in bytes of a Deploy when serialized via `ToBytes`. /// @@ -749,3 +757,49 @@ pub async fn get_era_info( .send_request(GET_ERA_INFO_METHOD, params) .await } + +/// Verifies the smart contract code against the one deployed at given deploy or transaction hash. +#[cfg(any(feature = "std-fs-io", test))] +pub async fn verify_contract( + hash_str: &str, + verification_url_base_path: &str, + project_path: Option<&str>, + verbosity: Verbosity, +) -> Result { + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Hash: {hash_str}"); + 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( + hash_str, + verification_url_base_path, + STANDARD.encode(&archive), + verbosity, + ) + .await +} diff --git a/lib/rpcs.rs b/lib/rpcs.rs index 8d0a3617..fd776795 100644 --- a/lib/rpcs.rs +++ b/lib/rpcs.rs @@ -14,7 +14,7 @@ pub(crate) mod v2_0_0; pub use v2_0_0::{ get_account::AccountIdentifier, get_dictionary_item::DictionaryItemIdentifier, - get_entity::{EntityIdentifier, EntityOrAccount}, + get_entity::{AddressableEntity, EntityIdentifier, EntityOrAccount}, get_reward::EraIdentifier, query_balance::PurseIdentifier, query_global_state::GlobalStateIdentifier, diff --git a/lib/rpcs/v2_0_0/get_entity.rs b/lib/rpcs/v2_0_0/get_entity.rs index b53df814..78c990ab 100644 --- a/lib/rpcs/v2_0_0/get_entity.rs +++ b/lib/rpcs/v2_0_0/get_entity.rs @@ -22,11 +22,26 @@ pub enum EntityIdentifier { EntityAddr(EntityAddr), } -/// An addressable entity with named keys and entry points. +/// Represents an entity that is addressable within the Casper system. +/// This struct holds the entity, its associated named keys, and its +/// entry points for interaction. +/// +/// # Fields +/// +/// * `entity` - The addressable entity which could be a contract, account, or other entity types in Casper. +/// * `named_keys` - A collection of named keys that are associated with the entity. Named keys +/// are a mapping from string identifiers to keys (e.g., contracts, URefs, etc.). +/// * `entry_points` - A list of entry points representing methods or functions that the entity exposes. +/// Entry points define the public interface of a contract or other executable object. #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct AddressableEntity { + /// The core addressable entity in Casper (account, contract, etc.). pub entity: CasperTypesAddressableEntity, + + /// The named keys associated with the entity, mapping identifiers to actual keys. pub named_keys: NamedKeys, + + /// A collection of entry points for the entity, defining its public interface. pub entry_points: Vec, } diff --git a/lib/rpcs/v2_0_0/get_transaction.rs b/lib/rpcs/v2_0_0/get_transaction.rs index eda9057f..0678b433 100644 --- a/lib/rpcs/v2_0_0/get_transaction.rs +++ b/lib/rpcs/v2_0_0/get_transaction.rs @@ -38,9 +38,9 @@ impl GetTransactionParams { #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct ExecutionInfo { - pub(crate) block_hash: BlockHash, - pub(crate) block_height: u64, - pub(crate) execution_result: Option, + pub block_hash: BlockHash, + pub block_height: u64, + pub execution_result: Option, } /// Result for "info_get_transaction" RPC response. diff --git a/lib/verification.rs b/lib/verification.rs new file mode 100644 index 00000000..f75d5a24 --- /dev/null +++ b/lib/verification.rs @@ -0,0 +1,218 @@ +use std::{cmp::min, io, path::Path}; + +use bytes::{BufMut, Bytes, BytesMut}; +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 transaction hash. +/// +/// Sends a verification request to the specified verification URL base path, including the +/// transaction hash, public key, and code archive. +/// +/// # Arguments +/// +/// * `hash_str` - The hash of the deploy or transaction that installed the 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( + hash_str: &str, + base_url: &str, + code_archive: String, + verbosity: Verbosity, +) -> Result { + let verification_request = VerificationRequest { + hash: hash_str.to_string(), + 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, hash_str, verbosity).await; + + if verbosity == Verbosity::Medium || verbosity == Verbosity::High { + println!("Getting verification details..."); + } + + let details_url = format!("{}/verification/{}/details", base_url, hash_str); + match http_client.get(details_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, + hash_str: &str, + 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, hash_str).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, + hash_str: &str, +) -> Result { + let status_url = format!("{}/verification/{}/status", base_url, hash_str); + let response = match http_client.get(status_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..f1928cbf --- /dev/null +++ b/lib/verification_types.rs @@ -0,0 +1,59 @@ +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 { + // Transaction hash of the contract deployment transaction. + pub(crate) hash: String, + // 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/rust-toolchain.toml b/rust-toolchain.toml index 8142c301..6f14058b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.73.0" +channel = "1.77.2" diff --git a/src/common.rs b/src/common.rs index fec06b85..b1a69cea 100644 --- a/src/common.rs +++ b/src/common.rs @@ -443,3 +443,51 @@ pub mod era_identifier { .unwrap_or_default() } } + +/// 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)) + } +} + +/// Handles providing the arg for and retrieval of the transaction hash. +pub mod transaction_hash { + use super::*; + + const ARG_NAME: &str = "transaction-hash"; + const ARG_VALUE_NAME: &str = "HEX STRING"; + const ARG_HELP: &str = "Hex-encoded transaction 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 7ce0a05d..58e4e53b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod query_balance; mod query_balance_details; mod query_global_state; mod transaction; +mod verify_contract; use std::process; @@ -58,6 +59,7 @@ use list_rpcs::ListRpcs; use query_balance::QueryBalance; use query_global_state::QueryGlobalState; use transaction::{MakeTransaction, PutTransaction, SendTransaction, SignTransaction}; +use verify_contract::VerifyContract; const APP_NAME: &str = "Casper client"; @@ -112,6 +114,7 @@ enum DisplayOrder { Keygen, AccountAddress, GenerateCompletion, + VerifyContract, } fn cli() -> Command { @@ -173,6 +176,7 @@ fn cli() -> Command { .subcommand(GenerateCompletion::build( DisplayOrder::GenerateCompletion as usize, )) + .subcommand(VerifyContract::build(DisplayOrder::VerifyContract as usize)) } #[tokio::main(flavor = "current_thread")] @@ -220,6 +224,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!(); @@ -229,7 +234,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/transaction/get.rs b/src/transaction/get.rs index 7d620ac8..b0ceed19 100644 --- a/src/transaction/get.rs +++ b/src/transaction/get.rs @@ -5,7 +5,11 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use casper_client::cli::CliError; -use crate::{command::ClientCommand, common, Success}; +use crate::{ + command::ClientCommand, + common::{self, transaction_hash}, + Success, +}; pub struct GetTransaction; @@ -19,29 +23,6 @@ enum DisplayOrder { } const ALIAS: &str = "get-txn"; -/// Handles providing the arg for and retrieval of the transaction hash. -mod transaction_hash { - use super::*; - - const ARG_NAME: &str = "transaction-hash"; - const ARG_VALUE_NAME: &str = "HEX STRING"; - const ARG_HELP: &str = "Hex-encoded transaction hash"; - - pub(super) fn arg() -> Arg { - Arg::new(ARG_NAME) - .required(true) - .value_name(ARG_VALUE_NAME) - .help(ARG_HELP) - .display_order(DisplayOrder::TransactionHash 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 { @@ -87,7 +68,9 @@ impl ClientCommand for GetTransaction { DisplayOrder::NodeAddress as usize, )) .arg(common::rpc_id::arg(DisplayOrder::RpcId as usize)) - .arg(transaction_hash::arg()) + .arg(transaction_hash::arg( + DisplayOrder::TransactionHash as usize, + )) .arg(finalized_approvals::arg()) } diff --git a/src/verify_contract.rs b/src/verify_contract.rs new file mode 100644 index 00000000..5cf9110d --- /dev/null +++ b/src/verify_contract.rs @@ -0,0 +1,103 @@ +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, + TransactionHash, + 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::transaction_hash::arg( + DisplayOrder::TransactionHash 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 transaction_hash = common::transaction_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( + transaction_hash, + verification_url_base_path, + verification_project_path, + verbosity_level, + ) + .await + .map(Success::from) + } +}