Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add voyager verification to the verify command #2566

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Cast

#### Added
- Add Voyager API support for `verify` subcommand [Read more here](https://foundry-rs.github.io/starknet-foundry/appendix/sncast/verify.html).

Comment on lines +12 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#### Added
- Add Voyager API support for `verify` subcommand [Read more here](https://foundry-rs.github.io/starknet-foundry/appendix/sncast/verify.html).
#### Added
- Voyager API support for `verify` subcommand [Read more here](https://foundry-rs.github.io/starknet-foundry/appendix/sncast/verify.html).

#### Changed

- Short option for `--accounts-file` flag has been removed.
Expand Down
11 changes: 11 additions & 0 deletions crates/sncast/src/helpers/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ pub struct CastConfig {
)]
/// Print links pointing to pages with transaction details in the chosen block explorer
pub show_explorer_links: bool,

#[serde(
default,
rename(
serialize = "verification-base-url",
deserialize = "verification-base-url"
)
)]
/// Custom base url to be used for verification
pub verification_base_url: Option<String>,
Comment on lines +50 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to allow overriding the URL, I think it should be done only in verify subcommand arguments not in a global configuration.

}

impl Default for CastConfig {
Expand All @@ -58,6 +68,7 @@ impl Default for CastConfig {
wait_params: ValidatedWaitParams::default(),
block_explorer: Some(block_explorer::Service::default()),
show_explorer_links: true,
verification_base_url: None,
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions crates/sncast/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use sncast::{
use starknet::core::utils::get_selector_from_name;
use starknet::providers::Provider;
use starknet_commands::account::list::print_account_list;
use starknet_commands::verify::Verify;
use starknet_commands::verification::verify::Verify;
use tokio::runtime::Runtime;

mod starknet_commands;
Expand Down Expand Up @@ -485,9 +485,11 @@ async fn run_async_command(
false,
)
.expect("Failed to build contract");
let result = starknet_commands::verify::verify(
let result = starknet_commands::verification::verify::verify(
&config,
verify.contract_address,
verify.contract_name,
verify.class_hash,
verify.class_name,
verify.verifier,
verify.network,
verify.confirm_verification,
Expand Down
2 changes: 1 addition & 1 deletion crates/sncast/src/starknet_commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ pub mod multicall;
pub mod script;
pub mod show_config;
pub mod tx_status;
pub mod verify;
pub mod verification;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't rename that. Modules in starknet_commands generally follow the name of the commands they are related to.

97 changes: 97 additions & 0 deletions crates/sncast/src/starknet_commands/verification/base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use camino::Utf8PathBuf;
use reqwest::StatusCode;
use serde::Serialize;
use sncast::{helpers::configuration::CastConfig, response::structs::VerifyResponse};
use starknet::core::types::Felt;
use std::ffi::OsStr;
use walkdir::WalkDir;

#[async_trait]
pub trait VerificationInterface {
fn get_workspace_dir(&self) -> Utf8PathBuf;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be a part of trait. I think all verification providers will want to handle it the same so there's no point abstracting it like that.

I'd simply change verify signature so it takes this directory as well.

Additionally read_workspace_files should be removed from this trait and should be made a free function that takes directory as an argument.


fn gen_explorer_url(&self, config: CastConfig) -> String;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of having an config argument, implementations should take a base url and network arguments when being created with new.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way usage is much simpler: provide an relevant override of gen_explorer_url that forms correct urls from information you have and verify can use it without providing anything.


fn read_workspace_files(&self) -> Result<serde_json::Map<String, serde_json::Value>> {
// 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
let workspace_dir = self.get_workspace_dir();
for entry in WalkDir::new(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(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),
);
}
}
}
}
Ok(file_data)
}

async fn send_verification_request(
&self,
url: String,
payload: VerificationPayload,
) -> Result<VerifyResponse> {
let json_payload = serde_json::to_string(&payload)?;
let client = reqwest::Client::new();
let api_res = client
.post(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))
}
}
Comment on lines +44 to +69
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't using self anyway, I'd just make it a normal function.


async fn verify(
&self,
config: &CastConfig,
contract_address: Option<Felt>,
class_hash: Option<Felt>,
class_name: String,
) -> Result<VerifyResponse> {
let file_data = self.read_workspace_files()?;
let source_code = serde_json::Value::Object(file_data);
let payload = VerificationPayload {
class_name,
contract_address,
class_hash,
source_code,
};
let url = self.gen_explorer_url(config.clone());
self.send_verification_request(url, payload).await
}
}

#[derive(Serialize, Debug)]
pub struct VerificationPayload {
pub class_name: String,
pub contract_address: Option<Felt>,
pub class_hash: Option<Felt>,
pub source_code: serde_json::Value,
}
4 changes: 4 additions & 0 deletions crates/sncast/src/starknet_commands/verification/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod base;
pub mod verify;
mod voyager;
mod walnut;
117 changes: 117 additions & 0 deletions crates/sncast/src/starknet_commands/verification/verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use super::base::VerificationInterface;
use super::voyager::VoyagerVerificationInterface;
use super::walnut::WalnutVerificationInterface;
use anyhow::{anyhow, bail, Result};
use camino::Utf8PathBuf;
use clap::{Parser, ValueEnum};
use promptly::prompt;
use scarb_api::StarknetContractArtifacts;
use sncast::helpers::configuration::CastConfig;
use sncast::response::structs::VerifyResponse;
use sncast::Network;
use starknet::core::types::Felt;
use std::collections::HashMap;
use std::fmt;

#[derive(Parser)]
#[command(about = "Verify a contract through a block explorer")]
pub struct Verify {
/// Contract address of the contract. Either this or class hash should be provided.
#[clap(short = 'd', long)]
pub contract_address: Option<Felt>,

/// Class hash of the contract. Either this or contract address should be provided
#[clap(short = 'x', long)]
pub class_hash: Option<Felt>,

/// Class name of the contract to be verified
#[clap(short, long)]
pub class_name: String,

/// Where you want your contract to be verified
#[clap(short, long, value_enum, default_value_t = Verifier::Walnut)]
pub verifier: Verifier,

/// The network in which the contract is deployed
#[clap(short, long, value_enum)]
pub network: Network,

/// Automatic yes to confirmation prompts for verification
#[clap(long, default_value = "false")]
pub confirm_verification: bool,

/// Optionally specify package with the contract to be verified
#[clap(long)]
pub package: Option<String>,
}

#[derive(ValueEnum, Clone, Debug)]
pub enum Verifier {
Walnut,
Voyager,
}

impl fmt::Display for Verifier {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Verifier::Walnut => write!(f, "walnut"),
Verifier::Voyager => write!(f, "voyager"),
}
}
}

// disable too many arguments clippy warning
#[allow(clippy::too_many_arguments)]
pub async fn verify(
cast_config: &CastConfig,
contract_address: Option<Felt>,
class_hash: Option<Felt>,
class_name: String,
verifier: Verifier,
network: Network,
confirm_verification: bool,
manifest_path: &Utf8PathBuf,
artifacts: &HashMap<String, StarknetContractArtifacts>,
) -> Result<VerifyResponse> {
// 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(&class_name) {
return Err(anyhow!("Contract named '{class_name}' was not found"));
}

// ensure that either contract_address or class_hash is provided
if contract_address.is_none() && class_hash.is_none() {
bail!("Either contract_address or class_hash must be provided");
}
Comment on lines +92 to +95
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be done at clap level with ArgGroup and flatten

Take a look at this: https://stackoverflow.com/questions/76315540/how-do-i-require-one-of-the-two-clap-options


// 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(cast_config, contract_address, class_hash, class_name)
.await
}
Verifier::Voyager => {
let voyager = VoyagerVerificationInterface::new(network, workspace_dir.to_path_buf());
voyager
.verify(cast_config, contract_address, class_hash, class_name)
.await
}
}
}
37 changes: 37 additions & 0 deletions crates/sncast/src/starknet_commands/verification/voyager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use super::base::VerificationInterface;
use async_trait::async_trait;
use camino::Utf8PathBuf;
use sncast::{helpers::configuration::CastConfig, Network};

pub struct VoyagerVerificationInterface {
pub network: Network,
pub workspace_dir: Utf8PathBuf,
}

impl VoyagerVerificationInterface {
pub fn new(network: Network, workspace_dir: Utf8PathBuf) -> Self {
VoyagerVerificationInterface {
network,
workspace_dir,
}
}
}

#[async_trait]
impl VerificationInterface for VoyagerVerificationInterface {
fn get_workspace_dir(&self) -> Utf8PathBuf {
self.workspace_dir.clone()
}

fn gen_explorer_url(&self, config: CastConfig) -> String {
let base_api_url = match config.verification_base_url {
Some(custom_base_api_url) => custom_base_api_url.clone(),
None => match self.network {
Network::Mainnet => "https://api.voyager.online/beta".to_string(),
Network::Sepolia => "https://sepolia-api.voyager.online/beta".to_string(),
},
};

format!("{base_api_url}/class-verify-v2")
}
}
38 changes: 38 additions & 0 deletions crates/sncast/src/starknet_commands/verification/walnut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use super::base::VerificationInterface;
use async_trait::async_trait;
use camino::Utf8PathBuf;
use sncast::{helpers::configuration::CastConfig, Network};

pub struct WalnutVerificationInterface {
pub network: Network,
pub workspace_dir: Utf8PathBuf,
}

impl WalnutVerificationInterface {
pub fn new(network: Network, workspace_dir: Utf8PathBuf) -> Self {
WalnutVerificationInterface {
network,
workspace_dir,
}
}
}

#[async_trait]
impl VerificationInterface for WalnutVerificationInterface {
fn get_workspace_dir(&self) -> Utf8PathBuf {
self.workspace_dir.clone()
}

fn gen_explorer_url(&self, config: CastConfig) -> String {
let api_base_url = match config.verification_base_url {
Some(custom_base_api_url) => custom_base_api_url.clone(),
None => "https://api.walnut.dev".to_string(),
};

let path = match self.network {
Network::Mainnet => "/v1/sn_main/verify",
Network::Sepolia => "/v1/sn_sepolia/verify",
};
format!("{api_base_url}{path}")
}
}
Loading