From c633861937bc7076bd79a4450ca9f4a1b5f344b0 Mon Sep 17 00:00:00 2001 From: Roman <30427286+barabanovro@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:49:49 +0400 Subject: [PATCH] Contract verification (#2252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #892 ## Introduced changes This PR builds upon the changes introduced in PR #875. The original PR is still open because the Voyager APIs are not ready yet, but we require the verification feature for the Walnut Debugger. To accommodate this, we have incorporated the changes from PR #875 and made the following additions: * Introduced the `WalnutVerificationInterface`. * Resolved branch conflicts. * Made minor modifications to ensure compatibility. Furthermore, to make the PR merge-ready, we have temporarily removed the Voyager and Starkscan interfaces since their respective APIs are not currently available. New verification interfaces can be added once Voyager, Starkscan, or any other provider adds API support for Cairo contract verification. This Pull Request adds the verify command for the contract verification. More about the details of the contract verification can found [here](https://github.com/foundry-rs/starknet-foundry/blob/master/design_documents/contract_verification.md). * Added `verify` command to the `sncast` tool * Modified `cast/main.rs` & created `cast/src/starknet_commands/verify.rs` mainly ## Checklist - [x] Linked relevant issue - [x] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [x] Added changes to `CHANGELOG.md` --------- Co-authored-by: MBarwicki Co-authored-by: Rohit Ranjan Co-authored-by: Tomasz Rejowski <34059895+Arcticae@users.noreply.github.com> Co-authored-by: Karol SewiƂo <95349104+ksew1@users.noreply.github.com> --- CHANGELOG.md | 6 + Cargo.lock | 85 ++++- Cargo.toml | 1 + crates/sncast/Cargo.toml | 2 + crates/sncast/src/lib.rs | 8 + crates/sncast/src/main.rs | 30 ++ crates/sncast/src/response/structs.rs | 7 + crates/sncast/src/starknet_commands/mod.rs | 1 + crates/sncast/src/starknet_commands/verify.rs | 200 ++++++++++ crates/sncast/tests/e2e/mod.rs | 1 + crates/sncast/tests/e2e/verify.rs | 359 ++++++++++++++++++ docs/src/SUMMARY.md | 1 + docs/src/appendix/sncast/verify.md | 37 ++ 13 files changed, 736 insertions(+), 2 deletions(-) create mode 100644 crates/sncast/src/starknet_commands/verify.rs create mode 100644 crates/sncast/tests/e2e/verify.rs create mode 100644 docs/src/appendix/sncast/verify.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8edb2c9f..2a063617a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Cast + +#### Added + +- `verify` subcommand to verify contract (walnut APIs supported as of this version). [Read more here](./docs/src/appendix/sncast/verify.md) + ## [0.26.0] - 2024-07-03 ### Forge diff --git a/Cargo.lock b/Cargo.lock index cd16a846e3..d11537baff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,6 +278,16 @@ dependencies = [ "term", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_fs" version = "1.1.1" @@ -310,6 +320,12 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "auto_impl" version = "1.1.0" @@ -1579,6 +1595,24 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.7.8" @@ -2306,6 +2340,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.0.0", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2464,7 +2517,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.22", "http 0.2.11", "http-body 0.4.6", "httparse", @@ -2487,6 +2540,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.5", "http 1.0.0", "http-body 1.0.0", "httparse", @@ -2494,6 +2548,7 @@ dependencies = [ "itoa", "pin-project-lite", "tokio", + "want", ] [[package]] @@ -3810,7 +3865,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.22", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.27", @@ -4528,6 +4583,8 @@ dependencies = [ "tokio", "toml", "url", + "walkdir", + "wiremock", ] [[package]] @@ -5774,6 +5831,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "wiremock" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec874e1eef0df2dcac546057fe5e29186f09c378181cd7b635b4b7bcc98e9d81" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.5", + "deadpool", + "futures", + "http 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index fe3453f87a..8bbea9248b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,3 +115,4 @@ base16ct = { version = "0.2.0", features = ["alloc"] } fs4 = "0.7" async-trait = "0.1.80" serde_path_to_error = "0.1.16" +wiremock = "0.6.0" \ No newline at end of file diff --git a/crates/sncast/Cargo.toml b/crates/sncast/Cargo.toml index a7e8d7b7b6..8715cd8ee2 100644 --- a/crates/sncast/Cargo.toml +++ b/crates/sncast/Cargo.toml @@ -47,6 +47,7 @@ base16ct.workspace = true starknet-crypto.workspace = true async-trait.workspace = true serde_path_to_error.workspace = true +walkdir.workspace = true [dev-dependencies] ctor.workspace = true @@ -56,6 +57,7 @@ project-root.workspace = true tempfile.workspace = true test-case.workspace = true fs_extra.workspace = true +wiremock.workspace = true [[bin]] name = "sncast" diff --git a/crates/sncast/src/lib.rs b/crates/sncast/src/lib.rs index 6bd2c1ee18..c1d7609e4e 100644 --- a/crates/sncast/src/lib.rs +++ b/crates/sncast/src/lib.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, bail, Context, Error, Result}; use camino::Utf8PathBuf; +use clap::ValueEnum; use helpers::constants::{KEYSTORE_PASSWORD_ENV_VAR, UDC_ADDRESS}; use rand::rngs::OsRng; use rand::RngCore; @@ -61,6 +62,13 @@ impl FromStr for AccountType { } } } + +#[derive(ValueEnum, Clone, Debug)] +pub enum Network { + Mainnet, + Sepolia, +} + #[derive(Deserialize, Serialize, Clone, Debug)] pub struct AccountData { pub private_key: FieldElement, diff --git a/crates/sncast/src/main.rs b/crates/sncast/src/main.rs index 126b2893e8..6d5be21b38 100644 --- a/crates/sncast/src/main.rs +++ b/crates/sncast/src/main.rs @@ -25,6 +25,7 @@ use sncast::{ use starknet::core::utils::get_selector_from_name; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; +use starknet_commands::verify::Verify; use tokio::runtime::Runtime; mod starknet_commands; @@ -141,6 +142,9 @@ enum Commands { /// Get the status of a transaction TxStatus(TxStatus), + + /// Verify a contract + Verify(Verify), } fn main() -> Result<()> { @@ -426,6 +430,32 @@ async fn run_async_command( print_command_result("tx-status", &mut result, numbers_format, &output_format)?; Ok(()) } + Commands::Verify(verify) => { + let manifest_path = assert_manifest_path_exists()?; + let package_metadata = get_package_metadata(&manifest_path, &verify.package)?; + let artifacts = build_and_load_artifacts( + &package_metadata, + &BuildConfig { + scarb_toml_path: manifest_path.clone(), + json: cli.json, + profile: cli.profile.unwrap_or("dev".to_string()), + }, + ) + .expect("Failed to build contract"); + let mut result = starknet_commands::verify::verify( + verify.contract_address, + verify.contract_name, + verify.verifier, + verify.network, + verify.confirm_verification, + &package_metadata.manifest_path, + &artifacts, + ) + .await; + + print_command_result("verify", &mut result, numbers_format, &output_format)?; + Ok(()) + } Commands::Script(_) => unreachable!(), } } diff --git a/crates/sncast/src/response/structs.rs b/crates/sncast/src/response/structs.rs index 35471eb0a3..746deb0826 100644 --- a/crates/sncast/src/response/structs.rs +++ b/crates/sncast/src/response/structs.rs @@ -144,3 +144,10 @@ pub struct TransactionStatusResponse { } impl CommandResponse for TransactionStatusResponse {} + +#[derive(Serialize)] +pub struct VerifyResponse { + pub message: String, +} + +impl CommandResponse for VerifyResponse {} diff --git a/crates/sncast/src/starknet_commands/mod.rs b/crates/sncast/src/starknet_commands/mod.rs index 3d3b6d9d74..4a96b853bc 100644 --- a/crates/sncast/src/starknet_commands/mod.rs +++ b/crates/sncast/src/starknet_commands/mod.rs @@ -7,3 +7,4 @@ pub mod multicall; pub mod script; pub mod show_config; pub mod tx_status; +pub mod verify; diff --git a/crates/sncast/src/starknet_commands/verify.rs b/crates/sncast/src/starknet_commands/verify.rs new file mode 100644 index 0000000000..41f57238fb --- /dev/null +++ b/crates/sncast/src/starknet_commands/verify.rs @@ -0,0 +1,200 @@ +use anyhow::{anyhow, Context, Result}; +use anyhow::{bail, Ok}; +use camino::Utf8PathBuf; +use clap::{Args, ValueEnum}; +use promptly::prompt; +use reqwest::StatusCode; +use scarb_api::StarknetContractArtifacts; +use serde::Serialize; +use sncast::response::structs::VerifyResponse; +use sncast::Network; +use starknet::core::types::FieldElement; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::{env, fmt}; +use walkdir::WalkDir; + +struct WalnutVerificationInterface { + network: Network, + workspace_dir: Utf8PathBuf, +} + +#[async_trait::async_trait] +trait VerificationInterface { + fn new(network: Network, workspace_dir: Utf8PathBuf) -> Self; + async fn verify( + &self, + contract_address: FieldElement, + contract_name: String, + ) -> Result; + fn gen_explorer_url(&self) -> Result; +} + +#[async_trait::async_trait] +impl VerificationInterface for WalnutVerificationInterface { + fn new(network: Network, workspace_dir: Utf8PathBuf) -> Self { + WalnutVerificationInterface { + network, + workspace_dir, + } + } + + async fn verify( + &self, + contract_address: FieldElement, + contract_name: String, + ) -> Result { + // Read all files name along with their contents in a JSON format + // in the workspace dir recursively + // key is the file name and value is the file content + let mut file_data = serde_json::Map::new(); + + // Recursively read files and their contents in workspace directory + for entry in WalkDir::new(self.workspace_dir.clone()).follow_links(true) { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + if let Some(extension) = path.extension() { + if extension == OsStr::new("cairo") || extension == OsStr::new("toml") { + let relative_path = path.strip_prefix(self.workspace_dir.clone())?; + let file_content = std::fs::read_to_string(path)?; + file_data.insert( + relative_path.to_string_lossy().into_owned(), + serde_json::Value::String(file_content), + ); + } + } + } + } + + // Serialize the JSON object to a JSON string + let source_code = serde_json::Value::Object(file_data); + + // Create the JSON payload with "contract name," "address," and "source_code" fields + let payload = VerificationPayload { + contract_name: contract_name.to_string(), + contract_address: contract_address.to_string(), + source_code, + }; + + // Serialize the payload to a JSON string for the POST request + let json_payload = serde_json::to_string(&payload)?; + + // Send the POST request to the explorer + let client = reqwest::Client::new(); + let api_res = client + .post(self.gen_explorer_url()?) + .header("Content-Type", "application/json") + .body(json_payload) + .send() + .await + .context("Failed to send request to verifier API")?; + + if api_res.status() == StatusCode::OK { + let message = api_res + .text() + .await + .context("Failed to read verifier API response")?; + Ok(VerifyResponse { message }) + } else { + let message = api_res.text().await.context("Failed to verify contract")?; + Err(anyhow!(message)) + } + } + + fn gen_explorer_url(&self) -> Result { + let api_base_url = + env::var("WALNUT_API_URL").unwrap_or_else(|_| "https://api.walnut.dev".to_string()); + let path = match self.network { + Network::Mainnet => "/v1/sn_main/verify", + Network::Sepolia => "/v1/sn_sepolia/verify", + }; + Ok(format!("{api_base_url}{path}")) + } +} + +#[derive(Args)] +#[command(about = "Verify a contract through a block explorer")] +pub struct Verify { + /// Address of a contract to be verified + #[clap(short = 'a', long)] + pub contract_address: FieldElement, + + /// Name of the contract that is being verified + #[clap(short, long)] + pub contract_name: String, + + /// Block explorer to use for the verification + #[clap(short, long, value_enum, default_value_t = Verifier::Walnut)] + pub verifier: Verifier, + + /// The network on which block explorer will do the verification + #[clap(short, long, value_enum)] + pub network: Network, + + /// Assume "yes" as answer to confirmation prompt and run non-interactively + #[clap(long, default_value = "false")] + pub confirm_verification: bool, + + /// Specifies scarb package to be used + #[clap(long)] + pub package: Option, +} + +#[derive(ValueEnum, Clone, Debug)] +pub enum Verifier { + Walnut, +} + +impl fmt::Display for Verifier { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Verifier::Walnut => write!(f, "walnut"), + } + } +} + +#[derive(Serialize, Debug)] +struct VerificationPayload { + contract_name: String, + contract_address: String, + source_code: serde_json::Value, +} + +pub async fn verify( + contract_address: FieldElement, + contract_name: String, + verifier: Verifier, + network: Network, + confirm_verification: bool, + manifest_path: &Utf8PathBuf, + artifacts: &HashMap, +) -> Result { + // Let's ask confirmation + if !confirm_verification { + let prompt_text = + format!("You are about to submit the entire workspace's code to the third-party chosen verifier at {verifier}, and the code will be publicly available through {verifier}'s APIs. Are you sure? (Y/n)"); + let input: String = prompt(prompt_text)?; + + if !input.starts_with('Y') { + bail!("Verification aborted"); + } + } + + if !artifacts.contains_key(&contract_name) { + return Err(anyhow!("Contract named '{contract_name}' was not found")); + } + + // Build JSON Payload for the verification request + // get the parent dir of the manifest path + let workspace_dir = manifest_path + .parent() + .ok_or(anyhow!("Failed to obtain workspace dir"))?; + + match verifier { + Verifier::Walnut => { + let walnut = WalnutVerificationInterface::new(network, workspace_dir.to_path_buf()); + walnut.verify(contract_address, contract_name).await + } + } +} diff --git a/crates/sncast/tests/e2e/mod.rs b/crates/sncast/tests/e2e/mod.rs index 0e0002474d..573cc5d827 100644 --- a/crates/sncast/tests/e2e/mod.rs +++ b/crates/sncast/tests/e2e/mod.rs @@ -8,3 +8,4 @@ mod multicall; mod script; mod show_config; mod tx_status; +mod verify; diff --git a/crates/sncast/tests/e2e/verify.rs b/crates/sncast/tests/e2e/verify.rs new file mode 100644 index 0000000000..c61c0bc563 --- /dev/null +++ b/crates/sncast/tests/e2e/verify.rs @@ -0,0 +1,359 @@ +use crate::helpers::constants::{CONTRACTS_DIR, MAP_CONTRACT_ADDRESS_SEPOLIA}; +use crate::helpers::fixtures::{copy_directory_to_tempdir, default_cli_args}; +use crate::helpers::runner::runner; +use indoc::formatdoc; +use shared::test_utils::output_assert::{assert_stderr_contains, assert_stdout_contains}; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +#[tokio::test] +async fn test_happy_case() { + let contract_path = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/map"); + + let mock_server = MockServer::start().await; + + let verifier_response = "Contract successfully verified"; + + Mock::given(method("POST")) + .and(path("/v1/sn_sepolia/verify")) + .respond_with( + ResponseTemplate::new(200) + .append_header("content-type", "text/plain") + .set_body_string(verifier_response), + ) + .mount(&mock_server) + .await; + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "Map", + "--verifier", + "walnut", + "--network", + "sepolia", + ]); + + let snapbox = runner(&args) + .env("WALNUT_API_URL", mock_server.uri()) + .current_dir(contract_path.path()) + .stdin("Y"); + + let output = snapbox.assert().success(); + + assert_stdout_contains( + output, + formatdoc!( + r" + command: verify + message: {} + ", + verifier_response + ), + ); +} + +#[tokio::test] +async fn test_failed_verification() { + let contract_path = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/map"); + + let mock_server = MockServer::start().await; + + let verifier_response = "An error occurred during verification: contract class isn't declared"; + + Mock::given(method("POST")) + .and(path("/v1/sn_sepolia/verify")) + .respond_with( + ResponseTemplate::new(400) + .append_header("content-type", "text/plain") + .set_body_string(verifier_response), + ) + .mount(&mock_server) + .await; + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "Map", + "--verifier", + "walnut", + "--network", + "sepolia", + ]); + + let snapbox = runner(&args) + .env("WALNUT_API_URL", mock_server.uri()) + .current_dir(contract_path.path()) + .stdin("Y"); + + let output = snapbox.assert().success(); + + assert_stderr_contains( + output, + formatdoc!( + r" + command: verify + error: {} + ", + verifier_response + ), + ); +} + +#[tokio::test] +async fn test_verification_abort() { + let contract_path = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/map"); + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "nonexistent", + "--verifier", + "walnut", + "--network", + "sepolia", + ]); + + let snapbox = runner(&args).current_dir(contract_path.path()).stdin("n"); + + let output = snapbox.assert().success(); + + assert_stderr_contains( + output, + formatdoc!( + r" + command: verify + error: Verification aborted + " + ), + ); +} + +#[tokio::test] +async fn test_wrong_contract_name_passed() { + let contract_path = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/map"); + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "nonexistent", + "--verifier", + "walnut", + "--network", + "sepolia", + ]); + + let snapbox = runner(&args).current_dir(contract_path.path()).stdin("Y"); + + let output = snapbox.assert().success(); + + assert_stderr_contains( + output, + formatdoc!( + r" + command: verify + error: Contract named 'nonexistent' was not found + " + ), + ); +} + +#[tokio::test] +async fn test_happy_case_with_confirm_verification_flag() { + let contract_path = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/map"); + + let mock_server = MockServer::start().await; + + let verifier_response = "Contract successfully verified"; + + Mock::given(method("POST")) + .and(path("/v1/sn_sepolia/verify")) + .respond_with( + ResponseTemplate::new(200) + .append_header("content-type", "text/plain") + .set_body_string(verifier_response), + ) + .mount(&mock_server) + .await; + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "Map", + "--verifier", + "walnut", + "--network", + "sepolia", + "--confirm-verification", + ]); + + let snapbox = runner(&args) + .env("WALNUT_API_URL", mock_server.uri()) + .current_dir(contract_path.path()); + + let output = snapbox.assert().success(); + + assert_stdout_contains( + output, + formatdoc!( + r" + command: verify + message: {} + ", + verifier_response + ), + ); +} + +#[tokio::test] +async fn test_happy_case_specify_package() { + let tempdir = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/multiple_packages"); + + let mock_server = MockServer::start().await; + + let verifier_response = "Contract successfully verified"; + + Mock::given(method("POST")) + .and(path("/v1/sn_sepolia/verify")) + .respond_with( + ResponseTemplate::new(200) + .append_header("content-type", "text/plain") + .set_body_string(verifier_response), + ) + .mount(&mock_server) + .await; + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "supercomplexcode", + "--verifier", + "walnut", + "--network", + "sepolia", + "--package", + "main_workspace", + ]); + + let snapbox = runner(&args) + .env("WALNUT_API_URL", mock_server.uri()) + .current_dir(tempdir.path()) + .stdin("Y"); + + let output = snapbox.assert().success(); + + assert_stdout_contains( + output, + formatdoc!( + r" + command: verify + message: {} + ", + verifier_response + ), + ); +} + +#[tokio::test] +async fn test_worskpaces_package_specified_virtual_fibonacci() { + let tempdir = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/virtual_workspace"); + + let mock_server = MockServer::start().await; + + let verifier_response = "Contract successfully verified"; + + Mock::given(method("POST")) + .and(path("/v1/sn_sepolia/verify")) + .respond_with( + ResponseTemplate::new(200) + .append_header("content-type", "text/plain") + .set_body_string(verifier_response), + ) + .mount(&mock_server) + .await; + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "FibonacciContract", + "--verifier", + "walnut", + "--network", + "sepolia", + "--package", + "cast_fibonacci", + ]); + + let snapbox = runner(&args) + .env("WALNUT_API_URL", mock_server.uri()) + .current_dir(tempdir.path()) + .stdin("Y"); + + let output = snapbox.assert().success(); + + assert_stdout_contains( + output, + formatdoc!( + r" + command: verify + message: {} + ", + verifier_response + ), + ); +} + +#[tokio::test] +async fn test_worskpaces_package_no_contract() { + let tempdir = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/virtual_workspace"); + + let mut args = default_cli_args(); + args.append(&mut vec![ + "verify", + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--contract-name", + "nonexistent", + "--verifier", + "walnut", + "--network", + "sepolia", + "--package", + "cast_addition", + ]); + + let snapbox = runner(&args).current_dir(tempdir.path()).stdin("Y"); + + let output = snapbox.assert().success(); + + assert_stderr_contains( + output, + formatdoc!( + r" + command: verify + error: Contract named 'nonexistent' was not found + " + ), + ); +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 92717a720e..4bb3e92262 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -109,6 +109,7 @@ * [init](appendix/sncast/script/init.md) * [run](appendix/sncast/script/run.md) * [tx-status](appendix/sncast/tx-status.md) + * [verify](appendix/sncast/verify.md) * [`sncast` Library Functions References](appendix/sncast-library.md) * [declare](appendix/sncast-library/declare.md) * [deploy](appendix/sncast-library/deploy.md) diff --git a/docs/src/appendix/sncast/verify.md b/docs/src/appendix/sncast/verify.md new file mode 100644 index 0000000000..7dcb27c67d --- /dev/null +++ b/docs/src/appendix/sncast/verify.md @@ -0,0 +1,37 @@ +# `verify` +Verify Cairo contract on a chosen verification provider. + +## `--contract-address, -a ` +Required. + +The address of the contract that is to be verified. + +## `--contract-name ` +Required. + +The name of the contract. The contract name is the part after the `mod` keyword in your contract file. + +## `--verifier, -v ` +Optional. + +The verification provider to use for the verification. Possible values are: +* `walnut` + +## `--network, -n ` +Required. + +The network on which block explorer will perform the verification. Possible values are: +* `mainnet` +* `sepolia` + +## `--package ` +Optional. + +Name of the package that should be used. + +If supplied, a contract from this package will be used. Required if more than one package exists in a workspace. + +## `--confirm-verification` +Optional. + +If passed, assume "yes" as answer to confirmation prompt and run non-interactively.