diff --git a/Cargo.lock b/Cargo.lock index cd2c8831f8..18f04e4d07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14066,14 +14066,14 @@ name = "sozo-walnut" version = "1.0.9" dependencies = [ "anyhow", + "clap", "console", - "dojo-world", + "dojo-utils", "reqwest 0.11.27", "scarb", "scarb-ui", "serde", "serde_json", - "sozo-scarbext", "starknet 0.12.0", "thiserror 1.0.63", "url", diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index c8510d1d16..fe5e37f62c 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -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; @@ -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())?, ); @@ -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(()) }) diff --git a/bin/sozo/src/commands/mod.rs b/bin/sozo/src/commands/mod.rs index 9ca42277f3..0170c6bc30 100644 --- a/bin/sozo/src/commands/mod.rs +++ b/bin/sozo/src/commands/mod.rs @@ -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"; @@ -65,6 +67,9 @@ pub enum Commands { Model(Box), #[command(about = "Inspect events emitted by the world")] Events(Box), + #[cfg(feature = "walnut")] + #[command(about = "Interact with walnut.dev - transactions debugger and simulator")] + Walnut(Box), } impl fmt::Display for Commands { @@ -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"), } } } @@ -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), } } diff --git a/crates/sozo/walnut/Cargo.toml b/crates/sozo/walnut/Cargo.toml index 0a9cf690eb..04ad812f0a 100644 --- a/crates/sozo/walnut/Cargo.toml +++ b/crates/sozo/walnut/Cargo.toml @@ -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 diff --git a/crates/sozo/walnut/src/debugger.rs b/crates/sozo/walnut/src/debugger.rs index 0edd38b80c..97d3ba546e 100644 --- a/crates/sozo/walnut/src/debugger.rs +++ b/crates/sozo/walnut/src/debugger.rs @@ -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)] @@ -26,7 +25,18 @@ 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(()) @@ -34,17 +44,7 @@ impl WalnutDebugger { /// 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 } } diff --git a/crates/sozo/walnut/src/lib.rs b/crates/sozo/walnut/src/lib.rs index 96feb2be92..08ec8d767a 100644 --- a/crates/sozo/walnut/src/lib.rs +++ b/crates/sozo/walnut/src/lib.rs @@ -25,6 +25,7 @@ mod debugger; mod transaction; mod utils; mod verification; +pub mod walnut; pub use debugger::WalnutDebugger; diff --git a/crates/sozo/walnut/src/utils.rs b/crates/sozo/walnut/src/utils.rs index ca861249c1..89da582579 100644 --- a/crates/sozo/walnut/src/utils.rs +++ b/crates/sozo/walnut/src/utils.rs @@ -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 { - 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()) diff --git a/crates/sozo/walnut/src/verification.rs b/crates/sozo/walnut/src/verification.rs index ca11be74ad..ab885ef950 100644 --- a/crates/sozo/walnut/src/verification.rs +++ b/crates/sozo/walnut/src/verification.rs @@ -3,67 +3,31 @@ 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(" "); @@ -71,46 +35,29 @@ pub async fn walnut_verify_migration_strategy( 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 { - 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, - /// The hashes of the Sierra classes. - pub class_hashes: Vec, - /// 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 { - 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 { + let res = + reqwest::Client::new().post(format!("{api_url}/v1/verify")).json(&payload).send().await?; if res.status() == StatusCode::OK { Ok(res.text().await?) @@ -119,7 +66,7 @@ async fn _verify_classes( } } -fn _collect_source_code(root_dir: &Path) -> Result { +fn collect_source_code(root_dir: &Path) -> Result { fn collect_files( root_dir: &Path, search_dir: &Path, diff --git a/crates/sozo/walnut/src/walnut.rs b/crates/sozo/walnut/src/walnut.rs new file mode 100644 index 0000000000..ced939d281 --- /dev/null +++ b/crates/sozo/walnut/src/walnut.rs @@ -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(()) + }) + } +}