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

feat: Introduce the sozo verify command + execute --walnut flag enabled #1

Open
wants to merge 5 commits into
base: main
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions bin/sozo/src/commands/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use dojo_world::config::calldata_decoder;
use scarb::core::Config;
use sozo_ops::resource_descriptor::ResourceDescriptor;
use sozo_scarbext::WorkspaceExt;
#[cfg(feature = "walnut")]
use sozo_walnut::WalnutDebugger;
use starknet::core::types::Call;
use starknet::core::utils as snutils;
Expand Down Expand Up @@ -67,7 +68,7 @@ impl ExecuteArgs {
let descriptor = self.tag_or_address.ensure_namespace(&profile_config.namespace.default);

#[cfg(feature = "walnut")]
let _walnut_debugger = WalnutDebugger::new_from_flag(
let walnut_debugger = WalnutDebugger::new_from_flag(
self.transaction.walnut,
self.starknet.url(profile_config.env.as_ref())?,
);
Expand Down Expand Up @@ -132,9 +133,13 @@ impl ExecuteArgs {
.await?;

let invoker = Invoker::new(&account, txn_config);
// TODO: add walnut back, perhaps at the invoker level.
let tx_result = invoker.invoke(call).await?;

#[cfg(feature = "walnut")]
if let Some(walnut_debugger) = walnut_debugger {
walnut_debugger.debug_transaction(&config.ui(), &tx_result)?;
}

println!("{}", tx_result);
Ok(())
})
Expand Down
9 changes: 9 additions & 0 deletions bin/sozo/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ use init::InitArgs;
use inspect::InspectArgs;
use migrate::MigrateArgs;
use model::ModelArgs;
#[cfg(feature = "walnut")]
use sozo_walnut::walnut::WalnutArgs;
use test::TestArgs;

#[derive(Debug, Subcommand)]
Expand Down Expand Up @@ -63,6 +65,9 @@ pub enum Commands {
Model(Box<ModelArgs>),
#[command(about = "Inspect events emitted by the world")]
Events(Box<EventsArgs>),
#[cfg(feature = "walnut")]
#[command(about = "Interact with walnut.dev - transactions debugger and simulator")]
Walnut(Box<WalnutArgs>),
}

impl fmt::Display for Commands {
Expand All @@ -81,6 +86,8 @@ impl fmt::Display for Commands {
Commands::Init(_) => write!(f, "Init"),
Commands::Model(_) => write!(f, "Model"),
Commands::Events(_) => write!(f, "Events"),
#[cfg(feature = "walnut")]
Commands::Walnut(_) => write!(f, "WalnutVerify"),
}
}
}
Expand All @@ -107,6 +114,8 @@ pub fn run(command: Commands, config: &Config) -> Result<()> {
Commands::Init(args) => args.run(config),
Commands::Model(args) => args.run(config),
Commands::Events(args) => args.run(config),
#[cfg(feature = "walnut")]
Commands::Walnut(args) => args.run(config),
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/sozo/walnut/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ version.workspace = true
[dependencies]
anyhow.workspace = true
console.workspace = true
dojo-world.workspace = true
reqwest.workspace = true
scarb.workspace = true
scarb-ui.workspace = true
serde.workspace = true
serde_json.workspace = true
sozo-scarbext.workspace = true
starknet.workspace = true
thiserror.workspace = true
url.workspace = true
urlencoding = "2.1.3"
walkdir.workspace = true
dojo-utils.workspace = true
clap.workspace = true

[dev-dependencies]
starknet.workspace = true
34 changes: 17 additions & 17 deletions crates/sozo/walnut/src/debugger.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use dojo_world::diff::WorldDiff;
use dojo_utils::TransactionResult;
use scarb::core::Workspace;
use scarb_ui::Ui;
use starknet::core::types::Felt;
use url::Url;

use crate::transaction::walnut_debug_transaction;
use crate::verification::walnut_verify_migration_strategy;
use crate::{utils, Error};
use crate::verification::walnut_verify;
use crate::Error;

/// A debugger for Starknet transactions embedding the walnut configuration.
#[derive(Debug)]
Expand All @@ -26,25 +25,26 @@ impl WalnutDebugger {
}

/// Debugs a transaction with Walnut by printing a link to the Walnut debugger page.
pub fn debug_transaction(&self, ui: &Ui, transaction_hash: &Felt) -> Result<(), Error> {
pub fn debug_transaction(
&self,
ui: &Ui,
transaction_result: &TransactionResult,
) -> Result<(), Error> {
let transaction_hash = match transaction_result {
TransactionResult::Hash(transaction_hash) => transaction_hash,
TransactionResult::Noop => {
return Ok(());
}
TransactionResult::HashReceipt(transaction_hash, _) => transaction_hash,
};
let url = walnut_debug_transaction(&self.rpc_url, transaction_hash)?;
ui.print(format!("Debug transaction with Walnut: {url}"));
Ok(())
}

/// Verifies a migration strategy with Walnut by uploading the source code of the contracts and
/// models in the strategy.
pub async fn verify_migration_strategy(
&self,
ws: &Workspace<'_>,
world_diff: &WorldDiff,
) -> anyhow::Result<()> {
walnut_verify_migration_strategy(ws, self.rpc_url.to_string(), world_diff).await
}

/// Checks if the Walnut API key is set.
pub fn check_api_key() -> Result<(), Error> {
let _ = utils::walnut_get_api_key()?;
Ok(())
pub async fn verify(ws: &Workspace<'_>) -> anyhow::Result<()> {
walnut_verify(ws).await
}
}
1 change: 1 addition & 0 deletions crates/sozo/walnut/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod debugger;
mod transaction;
mod utils;
mod verification;
pub mod walnut;

pub use debugger::WalnutDebugger;

Expand Down
6 changes: 1 addition & 5 deletions crates/sozo/walnut/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
use std::env;

use crate::{Error, WALNUT_API_KEY_ENV_VAR, WALNUT_API_URL, WALNUT_API_URL_ENV_VAR};

pub fn walnut_get_api_key() -> Result<String, Error> {
env::var(WALNUT_API_KEY_ENV_VAR).map_err(|_| Error::MissingApiKey)
}
use crate::{WALNUT_API_URL, WALNUT_API_URL_ENV_VAR};

pub fn walnut_get_api_url() -> String {
env::var(WALNUT_API_URL_ENV_VAR).unwrap_or_else(|_| WALNUT_API_URL.to_string())
Expand Down
119 changes: 33 additions & 86 deletions crates/sozo/walnut/src/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,114 +3,61 @@ use std::io;
use std::path::Path;

use console::{pad_str, Alignment, Style, StyledObject};
use dojo_world::diff::{ResourceDiff, WorldDiff};
use dojo_world::local::ResourceLocal;
use dojo_world::remote::ResourceRemote;
use dojo_world::ResourceType;
use reqwest::StatusCode;
use scarb::core::Workspace;
use serde::Serialize;
use serde_json::Value;
use sozo_scarbext::WorkspaceExt;
use walkdir::WalkDir;

use crate::utils::{walnut_get_api_key, walnut_get_api_url};
use crate::utils::walnut_get_api_url;
use crate::Error;

/// Verifies all classes declared during migration.
/// Only supported on hosted networks (non-localhost).
///
/// This function verifies all contracts and models in the strategy. For every contract and model,
/// it sends a request to the Walnut backend with the class name, class hash, RPC URL, and source
/// code. Walnut will then build the project with Sozo, compare the Sierra bytecode with the
/// bytecode on the network, and if they are equal, it will store the source code and associate it
/// with the class hash.
pub async fn walnut_verify_migration_strategy(
ws: &Workspace<'_>,
rpc_url: String,
world_diff: &WorldDiff,
) -> anyhow::Result<()> {
let ui = ws.config().ui();
// Check if rpc_url is localhost
if rpc_url.contains("localhost") || rpc_url.contains("127.0.0.1") {
ui.print(" ");
ui.warn("Verifying classes with Walnut is only supported on hosted networks.");
ui.print(" ");
return Ok(());
}

// Check if there are any contracts or models in the strategy
if world_diff.is_synced() {
ui.print(" ");
ui.print("🌰 No contracts or models to verify.");
ui.print(" ");
return Ok(());
}
#[derive(Debug, Serialize)]
struct VerificationPayload {
/// JSON that contains a map where the key is the path to the file and the value is the content
/// of the file. It should contain all files required to build the Dojo project with Sozo.
pub source_code: Value,

let _profile_config = ws.load_profile_config()?;
pub cairo_version: String,
}

for (_selector, resource) in world_diff.resources.iter() {
if resource.resource_type() == ResourceType::Contract {
match resource {
ResourceDiff::Created(ResourceLocal::Contract(_contract)) => {
// Need to verify created.
}
ResourceDiff::Updated(_, ResourceRemote::Contract(_contract)) => {
// Need to verify updated.
}
_ => {
// Synced, we don't need to verify.
}
}
}
}
/// Verifies all classes in the workspace.
///
/// This function verifies all contracts and models in the workspace. It sends a single request to
/// the Walnut backend with the source code. Walnut will then build the project and store
/// the source code associated with the class hashes.
pub async fn walnut_verify(ws: &Workspace<'_>) -> anyhow::Result<()> {
let ui = ws.config().ui();

// Notify start of verification
ui.print(" ");
ui.print("🌰 Verifying classes with Walnut...");
ui.print(" ");

// Retrieve the API key and URL from environment variables
let _api_key = walnut_get_api_key()?;
let _api_url = walnut_get_api_url();
let api_url = walnut_get_api_url();

// Collect source code
// TODO: now it's the same output as scarb, need to update the dojo fork to output the source
// code, or does scarb supports it already?
// its path to a file so `parent` should never return `None`
let root_dir: &Path = ws.manifest_path().parent().unwrap().as_std_path();

Ok(())
}
let source_code = collect_source_code(root_dir)?;
let cairo_version = scarb::version::get().version;

fn _get_class_name_from_artifact_path(path: &Path, namespace: &str) -> Result<String, Error> {
let file_name = path.file_stem().and_then(OsStr::to_str).ok_or(Error::InvalidFileName)?;
let class_name = file_name.strip_prefix(namespace).ok_or(Error::NamespacePrefixNotFound)?;
Ok(class_name.to_string())
}
let verification_payload =
VerificationPayload { source_code, cairo_version: cairo_version.to_string() };

#[derive(Debug, Serialize)]
struct _VerificationPayload {
/// The names of the classes we want to verify together with the selector.
pub class_names: Vec<String>,
/// The hashes of the Sierra classes.
pub class_hashes: Vec<String>,
/// The RPC URL of the network where these classes are declared (can only be a hosted network).
pub rpc_url: String,
/// JSON that contains a map where the key is the path to the file and the value is the content
/// of the file. It should contain all files required to build the Dojo project with Sozo.
pub source_code: Value,
// Send verification request
match verify_classes(verification_payload, &api_url).await {
Ok(message) => ui.print(_subtitle(message)),
Err(e) => ui.print(_subtitle(e.to_string())),
}

Ok(())
}

async fn _verify_classes(
payload: _VerificationPayload,
api_url: &str,
api_key: &str,
) -> Result<String, Error> {
let res = reqwest::Client::new()
.post(format!("{api_url}/v1/verify"))
.header("x-api-key", api_key)
.json(&payload)
.send()
.await?;
async fn verify_classes(payload: VerificationPayload, api_url: &str) -> Result<String, Error> {
let res =
reqwest::Client::new().post(format!("{api_url}/v1/verify")).json(&payload).send().await?;

if res.status() == StatusCode::OK {
Ok(res.text().await?)
Expand All @@ -119,7 +66,7 @@ async fn _verify_classes(
}
}

fn _collect_source_code(root_dir: &Path) -> Result<Value, Error> {
fn collect_source_code(root_dir: &Path) -> Result<Value, Error> {
fn collect_files(
root_dir: &Path,
search_dir: &Path,
Expand Down
36 changes: 36 additions & 0 deletions crates/sozo/walnut/src/walnut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use anyhow::Result;
use clap::{Args, Subcommand};
use scarb::core::Config;

use crate::WalnutDebugger;

#[derive(Debug, Args)]
pub struct WalnutArgs {
#[command(subcommand)]
pub command: WalnutVerifyCommand,
}

#[derive(Debug, Subcommand)]
pub enum WalnutVerifyCommand {
#[command(
about = "Verify contracts in walnut.dev - essential for debugging source code in Walnut"
)]
Verify(WalnutVerifyOptions),
}

#[derive(Debug, Args)]
pub struct WalnutVerifyOptions {}

impl WalnutArgs {
pub fn run(self, config: &Config) -> Result<()> {
let ws = scarb::ops::read_workspace(config.manifest_path(), config)?;
config.tokio_handle().block_on(async {
match self.command {
WalnutVerifyCommand::Verify(_options) => {
WalnutDebugger::verify(&ws).await?;
}
}
Ok(())
})
}
}
Loading