Skip to content

Commit

Permalink
feat(sozo): introduce walnut verify command + execute "--walnut" flag…
Browse files Browse the repository at this point in the history
… enabled (#2734)

* Introduce the verify command

* Fixes after review

* Fixes after review

* Fixes after review

* Fixes after review

---------

Co-authored-by: Roman <[email protected]>
Co-authored-by: mobycrypt <[email protected]>
  • Loading branch information
3 people authored Jan 6, 2025
1 parent 1ddee9e commit e6d82d9
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 114 deletions.
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;

pub(crate) const LOG_TARGET: &str = "sozo::cli";
Expand Down Expand Up @@ -65,6 +67,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 @@ -83,6 +88,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 @@ -109,6 +116,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(())
})
}
}

0 comments on commit e6d82d9

Please sign in to comment.