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

refactor verification crate & add voyager interface #2288

Closed
wants to merge 15 commits into from
7 changes: 4 additions & 3 deletions crates/sncast/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use sncast::{
};
use starknet::core::utils::get_selector_from_name;
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 @@ -465,9 +465,10 @@ async fn run_async_command(
},
)
.expect("Failed to build contract");
let mut result = starknet_commands::verify::verify(
let mut result = starknet_commands::verification::verify::verify(
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;
90 changes: 90 additions & 0 deletions crates/sncast/src/starknet_commands/verification/base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use camino::Utf8PathBuf;
use reqwest::StatusCode;
use serde::Serialize;
use sncast::response::structs::VerifyResponse;
use sncast::Network;
use starknet::core::types::FieldElement;
use std::ffi::OsStr;
use walkdir::WalkDir;

#[async_trait]
pub trait VerificationInterface {
fn new(network: Network, workspace_dir: Utf8PathBuf) -> Self;
async fn verify(
Copy link
Member

Choose a reason for hiding this comment

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

Both implementations of this method are identical. You can make it a default implementation

&self,
contract_address: Option<FieldElement>,
class_hash: Option<FieldElement>,
class_name: String,
) -> Result<VerifyResponse>;
fn gen_explorer_url(&self) -> Result<String>;
Copy link
Member

Choose a reason for hiding this comment

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

  1. Similarly to verify, implementations of this method differ only with the url source. How about introducing a new trait method, let's say get_base_url, implementing it manually on both types and using in a default implementation of gen_explorer_url?
  2. This method never fails. Are there any reasons for it to return a Result?

}

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

impl BaseVerificationInterface {
pub 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
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),
);
}
}
}
}
Ok(file_data)
}

pub 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))
}
}
}

#[derive(Serialize, Debug)]
pub struct VerificationPayload {
pub class_name: String,
pub contract_address: Option<FieldElement>,
pub class_hash: Option<FieldElement>,
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;
108 changes: 108 additions & 0 deletions crates/sncast/src/starknet_commands/verification/verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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::response::structs::VerifyResponse;
use sncast::Network;
use starknet::core::types::FieldElement;
use std::collections::HashMap;
use std::fmt;

#[derive(Parser)]
#[command(about = "Verify a contract through a block explorer")]
pub struct Verify {
Copy link
Member

Choose a reason for hiding this comment

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

Please add descriptions of all parameters to display in help

#[clap(short = 'a', long)]
pub contract_address: Option<FieldElement>,

#[clap(short = 'x', long)]
pub class_hash: Option<FieldElement>,

#[clap(short, long)]
pub class_name: String,

#[clap(short, long, value_enum, default_value_t = Verifier::Walnut)]
pub verifier: Verifier,

#[clap(short, long, value_enum)]
pub network: Network,

#[clap(long, default_value = "false")]
pub confirm_verification: bool,

#[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(
contract_address: Option<FieldElement>,
class_hash: Option<FieldElement>,
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");
}

// 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, class_hash, class_name)
.await
}
Verifier::Voyager => {
let voyager = VoyagerVerificationInterface::new(network, workspace_dir.to_path_buf());
voyager
.verify(contract_address, class_hash, class_name)
.await
}
}
}
52 changes: 52 additions & 0 deletions crates/sncast/src/starknet_commands/verification/voyager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use super::base::{BaseVerificationInterface, VerificationInterface, VerificationPayload};
use anyhow::Result;
use async_trait::async_trait;
use camino::Utf8PathBuf;
use sncast::response::structs::VerifyResponse;
use sncast::Network;
use starknet::core::types::FieldElement;
use std::env;

pub struct VoyagerVerificationInterface {
base: BaseVerificationInterface,
}

#[async_trait]
Copy link
Member

Choose a reason for hiding this comment

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

#[async_trait] attribute is not required

impl VerificationInterface for VoyagerVerificationInterface {
fn new(network: Network, workspace_dir: Utf8PathBuf) -> Self {
VoyagerVerificationInterface {
base: BaseVerificationInterface {
network,
workspace_dir,
},
}
}

async fn verify(
&self,
contract_address: Option<FieldElement>,
class_hash: Option<FieldElement>,
class_name: String,
) -> Result<VerifyResponse> {
let file_data = self.base.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()?;
self.base.send_verification_request(url, payload).await
}

fn gen_explorer_url(&self) -> Result<String> {
let api_base_url = env::var("VOYAGER_API_URL")
Copy link
Member

Choose a reason for hiding this comment

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

We would like to unify the wole configuration to a TOML file. Could you please redesign the url acquisition logic to use an enum provied via CastConfig rather than an environmental variable?

.unwrap_or_else(|_| "https://api.voyager.online/beta".to_string());
let path = match self.base.network {
Network::Mainnet => "/v1/sn_main/verify",
Network::Sepolia => "/v1/sn_sepolia/verify",
};
Ok(format!("{api_base_url}{path}"))
}
}
52 changes: 52 additions & 0 deletions crates/sncast/src/starknet_commands/verification/walnut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use super::base::{BaseVerificationInterface, VerificationInterface, VerificationPayload};
use anyhow::Result;
use async_trait::async_trait;
use camino::Utf8PathBuf;
use sncast::response::structs::VerifyResponse;
use sncast::Network;
use starknet::core::types::FieldElement;
use std::env;

pub struct WalnutVerificationInterface {
base: BaseVerificationInterface,
}

#[async_trait]
impl VerificationInterface for WalnutVerificationInterface {
fn new(network: Network, workspace_dir: Utf8PathBuf) -> Self {
WalnutVerificationInterface {
base: BaseVerificationInterface {
network,
workspace_dir,
},
}
}

async fn verify(
&self,
contract_address: Option<FieldElement>,
class_hash: Option<FieldElement>,
class_name: String,
) -> Result<VerifyResponse> {
let file_data = self.base.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()?;
self.base.send_verification_request(url, payload).await
}

fn gen_explorer_url(&self) -> Result<String> {
let api_base_url =
env::var("WALNUT_API_URL").unwrap_or_else(|_| "https://api.walnut.dev".to_string());
let path = match self.base.network {
Network::Mainnet => "/v1/sn_main/verify",
Network::Sepolia => "/v1/sn_sepolia/verify",
};
Ok(format!("{api_base_url}{path}"))
}
}
Loading
Loading