From 44b6bb0e7c81bf58228a2129d428179ac6ddf16a Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 01:43:12 +0100 Subject: [PATCH 01/18] sozo keystore, sozo account --- Cargo.lock | 55 ++- bin/sozo/Cargo.toml | 5 +- bin/sozo/src/args.rs | 6 + bin/sozo/src/commands/account.rs | 124 +++++++ bin/sozo/src/commands/keystore.rs | 104 ++++++ bin/sozo/src/commands/mod.rs | 4 + bin/sozo/src/commands/options/account.rs | 226 +----------- bin/sozo/src/commands/options/fee.rs | 101 ++++++ bin/sozo/src/commands/options/mod.rs | 2 + bin/sozo/src/commands/options/signer.rs | 237 +++++++++++++ crates/sozo/ops/Cargo.toml | 6 +- crates/sozo/ops/src/account.rs | 424 +++++++++++++++++++++++ crates/sozo/ops/src/keystore.rs | 140 ++++++++ crates/sozo/ops/src/lib.rs | 2 + 14 files changed, 1215 insertions(+), 221 deletions(-) create mode 100644 bin/sozo/src/commands/account.rs create mode 100644 bin/sozo/src/commands/keystore.rs create mode 100644 bin/sozo/src/commands/options/fee.rs create mode 100644 bin/sozo/src/commands/options/signer.rs create mode 100644 crates/sozo/ops/src/account.rs create mode 100644 crates/sozo/ops/src/keystore.rs diff --git a/Cargo.lock b/Cargo.lock index ba897b4e69..34d1ad10cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,6 +992,19 @@ dependencies = [ "serde", ] +[[package]] +name = "bigdecimal" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9324c8014cd04590682b34f1e9448d38f0674d0f7b2dc553331016ef0e4e9ebc" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits 0.2.17", +] + [[package]] name = "bincode" version = "1.3.3" @@ -2485,6 +2498,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "colored_json" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74cb9ce6b86f6e54bfa9518df2eeeef65d424ec7244d083ed97229185e366a91" +dependencies = [ + "is-terminal", + "serde", + "serde_json", + "yansi", +] + [[package]] name = "combine" version = "4.6.6" @@ -9892,6 +9917,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + [[package]] name = "rsa" version = "0.9.6" @@ -9979,6 +10015,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rtp" version = "0.9.0" @@ -11078,6 +11124,7 @@ dependencies = [ "anyhow", "assert_fs", "async-trait", + "bigdecimal 0.4.3", "cainome 0.1.5", "cairo-lang-compiler", "cairo-lang-defs", @@ -11104,6 +11151,8 @@ dependencies = [ "katana-runner", "notify", "notify-debouncer-mini", + "num-bigint", + "num-integer", "scarb", "scarb-ui", "semver 1.0.21", @@ -11144,6 +11193,8 @@ dependencies = [ "clap", "clap-verbosity-flag", "clap_complete", + "colored", + "colored_json", "console", "dojo-bindgen", "dojo-lang", @@ -11154,11 +11205,13 @@ dependencies = [ "katana-runner", "notify", "notify-debouncer-mini", + "rpassword", "scarb", "scarb-ui", "semver 1.0.21", "serde", "serde_json", + "serde_with", "smol_str", "snapbox", "starknet 0.9.0", @@ -11636,7 +11689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "067419451efdea1ee968df8438369960c167e0e905c05b84afd074f50e1d6f3d" dependencies = [ "ark-ff 0.4.2", - "bigdecimal", + "bigdecimal 0.3.1", "crypto-bigint", "getrandom", "hex", diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index fecb6807cc..cea62c5119 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -8,6 +8,7 @@ version.workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true +bigdecimal = "0.4.1" cairo-lang-compiler.workspace = true cairo-lang-defs.workspace = true cairo-lang-filesystem.workspace = true @@ -31,12 +32,15 @@ dojo-world = { workspace = true, features = [ "contracts", "metadata", "migratio futures.workspace = true notify = "6.0.1" notify-debouncer-mini = "0.3.0" +num-bigint = "0.4.3" +num-integer = "0.1.45" scarb-ui.workspace = true scarb.workspace = true semver.workspace = true serde.workspace = true serde_json.workspace = true smol_str.workspace = true +sozo-ops.workspace = true starknet-crypto.workspace = true starknet.workspace = true thiserror.workspace = true @@ -44,7 +48,6 @@ tokio.workspace = true tracing-log = "0.1.3" tracing.workspace = true url.workspace = true -sozo-ops.workspace = true cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.2" } diff --git a/bin/sozo/src/args.rs b/bin/sozo/src/args.rs index fc3c7c5f5b..7ba907fe40 100644 --- a/bin/sozo/src/args.rs +++ b/bin/sozo/src/args.rs @@ -7,6 +7,7 @@ use smol_str::SmolStr; use tracing::level_filters::LevelFilter; use tracing_log::AsTrace; +use crate::commands::account::AccountArgs; use crate::commands::auth::AuthArgs; use crate::commands::build::BuildArgs; use crate::commands::clean::CleanArgs; @@ -15,6 +16,7 @@ use crate::commands::dev::DevArgs; use crate::commands::events::EventsArgs; use crate::commands::execute::ExecuteArgs; use crate::commands::init::InitArgs; +use crate::commands::keystore::KeystoreArgs; use crate::commands::migrate::MigrateArgs; use crate::commands::model::ModelArgs; use crate::commands::register::RegisterArgs; @@ -51,6 +53,8 @@ pub struct SozoArgs { #[derive(Subcommand)] pub enum Commands { + #[command(about = "Manage accounts")] + Account(AccountArgs), #[command(about = "Build the world, generating the necessary artifacts for deployment")] Build(BuildArgs), #[command(about = "Initialize a new project")] @@ -74,6 +78,8 @@ pub enum Commands { Events(EventsArgs), #[command(about = "Manage world authorization")] Auth(AuthArgs), + #[clap(about = "Manage keystore files")] + Keystore(KeystoreArgs), #[command(about = "Generate shell completion file for specified shell")] Completions(CompletionsArgs), } diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs new file mode 100644 index 0000000000..e2c23e7401 --- /dev/null +++ b/bin/sozo/src/commands/account.rs @@ -0,0 +1,124 @@ +// MIT License + +// Copyright (c) 2022 Jonathan LEI + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use std::path::PathBuf; + +use anyhow::Result; +use clap::{Args, Subcommand}; +use scarb::core::Config; +use sozo_ops::account; +use starknet::signers::LocalWallet; +use starknet_crypto::FieldElement; + +use super::options::fee::FeeOptions; +use super::options::signer::SignerOptions; +use super::options::starknet::StarknetOptions; +use crate::utils; + +#[derive(Debug, Args)] +pub struct AccountArgs { + #[clap(subcommand)] + command: AccountCommand, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Subcommand)] +pub enum AccountCommand { + #[clap(about = "Create a new account configuration without actually deploying.")] + New { + #[clap(flatten)] + signer: SignerOptions, + + #[clap(long, short, help = "Overwrite the account config file if it already exists")] + force: bool, + + #[clap(help = "Path to save the account config file")] + output: PathBuf, + }, + + #[clap(about = "Deploy account contract with a DeployAccount transaction.")] + Deploy { + #[clap(flatten)] + starknet: StarknetOptions, + + #[clap(flatten)] + signer: SignerOptions, + + #[clap(flatten)] + fee: FeeOptions, + + #[clap(long, help = "Simulate the transaction only")] + simulate: bool, + + #[clap(long, help = "Provide transaction nonce manually")] + nonce: Option, + + #[clap( + long, + env = "STARKNET_POLL_INTERVAL", + default_value = "5000", + help = "Transaction result poll interval in milliseconds" + )] + poll_interval: u64, + + #[clap(help = "Path to the account config file")] + file: PathBuf, + }, +} + +impl AccountArgs { + pub fn run(self, config: &Config) -> Result<()> { + let env_metadata = utils::load_metadata_from_config(config)?; + + config.tokio_handle().block_on(async { + match self.command { + AccountCommand::New { signer, force, output } => { + let signer: LocalWallet = signer.signer(env_metadata.as_ref()).unwrap(); + account::new(signer, force, output).await + } + AccountCommand::Deploy { + starknet, + signer, + fee, + simulate, + nonce, + poll_interval, + file, + } => { + let provider = starknet.provider(env_metadata.as_ref()).unwrap(); + let signer = signer.signer(env_metadata.as_ref()).unwrap(); + let fee_setting = fee.into_setting()?; + account::deploy( + provider, + signer, + fee_setting, + simulate, + nonce, + poll_interval, + file, + ) + .await + } + } + }) + } +} diff --git a/bin/sozo/src/commands/keystore.rs b/bin/sozo/src/commands/keystore.rs new file mode 100644 index 0000000000..a04be6f5c7 --- /dev/null +++ b/bin/sozo/src/commands/keystore.rs @@ -0,0 +1,104 @@ +// MIT License + +// Copyright (c) 2022 Jonathan LEI + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use std::path::PathBuf; + +use anyhow::Result; +use clap::{Args, Subcommand}; +use sozo_ops::keystore; + +#[derive(Debug, Args)] +pub struct KeystoreArgs { + #[clap(subcommand)] + command: KeystoreCommand, +} + +#[derive(Debug, Subcommand)] +pub enum KeystoreCommand { + #[clap(about = "Randomly generate a new keystore.")] + New { + #[clap(long, help = "Supply password from command line option instead of prompt")] + password: Option, + + #[clap(long, help = "Overwrite the file if it already exists")] + force: bool, + + #[clap(help = "Path to save the JSON keystore")] + file: PathBuf, + }, + + #[clap(about = "Create a keystore file from an existing private key.")] + FromKey { + #[clap(long, help = "Overwrite the file if it already exists")] + force: bool, + + #[clap(long, help = "Take the private key from stdin instead of prompt")] + private_key_stdin: bool, + + #[clap(long, help = "Supply password from command line option instead of prompt")] + password: Option, + + #[clap(help = "Path to save the JSON keystore")] + file: PathBuf, + }, + + #[clap(about = "Check the public key of an existing keystore file.")] + Inspect { + #[clap(long, help = "Supply password from command line option instead of prompt")] + password: Option, + + #[clap(long, help = "Print the public key only")] + raw: bool, + + #[clap(help = "Path to the JSON keystore")] + file: PathBuf, + }, + + #[clap(about = "Check the private key of an existing keystore file.")] + InspectPrivate { + #[clap(long, help = "Supply password from command line option instead of prompt")] + password: Option, + + #[clap(long, help = "Print the private key only")] + raw: bool, + + #[clap(help = "Path to the JSON keystore")] + file: PathBuf, + }, +} + +impl KeystoreArgs { + pub fn run(self) -> Result<()> { + match self.command { + KeystoreCommand::New { password, force, file } => keystore::new(password, force, file), + KeystoreCommand::FromKey { force, private_key_stdin, password, file } => { + keystore::from_key(force, private_key_stdin, password, file) + } + KeystoreCommand::Inspect { password, raw, file } => { + keystore::inspect(password, raw, file) + } + KeystoreCommand::InspectPrivate { password, raw, file } => { + keystore::inspect_private(password, raw, file) + } + } + } +} diff --git a/bin/sozo/src/commands/mod.rs b/bin/sozo/src/commands/mod.rs index fce588da4f..e1a4e2753b 100644 --- a/bin/sozo/src/commands/mod.rs +++ b/bin/sozo/src/commands/mod.rs @@ -3,6 +3,7 @@ use scarb::core::Config; use crate::args::Commands; +pub(crate) mod account; pub(crate) mod auth; pub(crate) mod build; pub(crate) mod clean; @@ -11,6 +12,7 @@ pub(crate) mod dev; pub(crate) mod events; pub(crate) mod execute; pub(crate) mod init; +pub(crate) mod keystore; pub(crate) mod migrate; pub(crate) mod model; pub(crate) mod options; @@ -19,6 +21,7 @@ pub(crate) mod test; pub fn run(command: Commands, config: &Config) -> Result<()> { match command { + Commands::Account(args) => args.run(config), Commands::Init(args) => args.run(config), Commands::Clean(args) => args.run(config), Commands::Test(args) => args.run(config), @@ -30,6 +33,7 @@ pub fn run(command: Commands, config: &Config) -> Result<()> { Commands::Model(args) => args.run(config), Commands::Register(args) => args.run(config), Commands::Events(args) => args.run(config), + Commands::Keystore(args) => args.run(), Commands::Completions(args) => args.run(), } } diff --git a/bin/sozo/src/commands/options/account.rs b/bin/sozo/src/commands/options/account.rs index 1538e8f106..71ae8a3fd0 100644 --- a/bin/sozo/src/commands/options/account.rs +++ b/bin/sozo/src/commands/options/account.rs @@ -6,41 +6,20 @@ use dojo_world::metadata::Environment; use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::types::FieldElement; use starknet::providers::Provider; -use starknet::signers::{LocalWallet, SigningKey}; +use starknet::signers::LocalWallet; -use super::{ - DOJO_ACCOUNT_ADDRESS_ENV_VAR, DOJO_KEYSTORE_PASSWORD_ENV_VAR, DOJO_KEYSTORE_PATH_ENV_VAR, - DOJO_PRIVATE_KEY_ENV_VAR, -}; +use super::signer::SignerOptions; +use super::DOJO_ACCOUNT_ADDRESS_ENV_VAR; #[derive(Debug, Args)] #[command(next_help_heading = "Account options")] -// INVARIANT: -// - For commandline: we can either specify `private_key` or `keystore_path` along with -// `keystore_password`. This is enforced by Clap. -// - For `Scarb.toml`: if both private_key and keystore are specified in `Scarb.toml` private_key -// will take priority pub struct AccountOptions { #[arg(long, env = DOJO_ACCOUNT_ADDRESS_ENV_VAR)] pub account_address: Option, - #[arg(long, env = DOJO_PRIVATE_KEY_ENV_VAR)] - #[arg(conflicts_with = "keystore_path")] - #[arg(help_heading = "Signer options - RAW")] - #[arg(help = "The raw private key associated with the account contract.")] - pub private_key: Option, - - #[arg(long = "keystore", env = DOJO_KEYSTORE_PATH_ENV_VAR)] - #[arg(value_name = "PATH")] - #[arg(help_heading = "Signer options - KEYSTORE")] - #[arg(help = "Use the keystore in the given folder or file.")] - pub keystore_path: Option, - - #[arg(long = "password", env = DOJO_KEYSTORE_PASSWORD_ENV_VAR)] - #[arg(value_name = "PASSWORD")] - #[arg(help_heading = "Signer options - KEYSTORE")] - #[arg(help = "The keystore password. Used with --keystore.")] - pub keystore_password: Option, + #[command(flatten)] + #[command(next_help_heading = "Signer options")] + pub signer: SignerOptions, #[arg(long)] #[arg(help = "Use legacy account (cairo0 account)")] @@ -57,7 +36,7 @@ impl AccountOptions { P: Provider + Send + Sync, { let account_address = self.account_address(env_metadata)?; - let signer = self.signer(env_metadata)?; + let signer = self.signer.signer(env_metadata)?; let chain_id = provider.chain_id().await.with_context(|| "Failed to retrieve network chain id.")?; @@ -67,39 +46,6 @@ impl AccountOptions { Ok(SingleOwnerAccount::new(provider, signer, account_address, chain_id, encoding)) } - fn signer(&self, env_metadata: Option<&Environment>) -> Result { - if let Some(private_key) = - self.private_key.as_deref().or_else(|| env_metadata.and_then(|env| env.private_key())) - { - return Ok(LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - FieldElement::from_str(private_key)?, - ))); - } - - if let Some(path) = &self - .keystore_path - .as_deref() - .or_else(|| env_metadata.and_then(|env| env.keystore_path())) - { - if let Some(password) = self - .keystore_password - .as_deref() - .or_else(|| env_metadata.and_then(|env| env.keystore_password())) - { - return Ok(LocalWallet::from_signing_key(SigningKey::from_keystore( - path, password, - )?)); - } else { - return Err(anyhow!("Keystore path is specified but password is not.")); - } - } - - Err(anyhow!( - "Could not find private key. Please specify the private key or path to the keystore \ - file." - )) - } - fn account_address(&self, env_metadata: Option<&Environment>) -> Result { if let Some(address) = self.account_address { Ok(address) @@ -116,17 +62,11 @@ impl AccountOptions { #[cfg(test)] mod tests { - use std::str::FromStr; - use clap::Parser; use starknet::accounts::{Call, ExecutionEncoder}; - use starknet::signers::{LocalWallet, Signer, SigningKey}; use starknet_crypto::FieldElement; - use super::{ - AccountOptions, DOJO_ACCOUNT_ADDRESS_ENV_VAR, DOJO_KEYSTORE_PASSWORD_ENV_VAR, - DOJO_PRIVATE_KEY_ENV_VAR, - }; + use super::{AccountOptions, DOJO_ACCOUNT_ADDRESS_ENV_VAR}; #[derive(clap::Parser, Debug)] struct Command { @@ -142,22 +82,6 @@ mod tests { assert_eq!(cmd.account.account_address, Some(FieldElement::from_hex_be("0x0").unwrap())); } - #[test] - fn private_key_read_from_env_variable() { - std::env::set_var(DOJO_PRIVATE_KEY_ENV_VAR, "private_key"); - - let cmd = Command::parse_from(["sozo", "--account-address", "0x0"]); - assert_eq!(cmd.account.private_key, Some("private_key".to_owned())); - } - - #[test] - fn keystore_path_read_from_env_variable() { - std::env::set_var(DOJO_KEYSTORE_PASSWORD_ENV_VAR, "keystore_password"); - - let cmd = Command::parse_from(["sozo", "--keystore", "./some/path"]); - assert_eq!(cmd.account.keystore_password, Some("keystore_password".to_owned())); - } - #[test] fn account_address_from_args() { let cmd = Command::parse_from(["sozo", "--account-address", "0x0"]); @@ -201,122 +125,6 @@ mod tests { assert!(cmd.account.account_address(None).is_err()); } - #[tokio::test] - async fn private_key_from_args() { - let private_key = "0x1"; - - let cmd = - Command::parse_from(["sozo", "--account-address", "0x0", "--private-key", private_key]); - let result_wallet = cmd.account.signer(None).unwrap(); - let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - FieldElement::from_str(private_key).unwrap(), - )); - - let result_public_key = result_wallet.get_public_key().await.unwrap(); - let expected_public_key = expected_wallet.get_public_key().await.unwrap(); - assert!(result_public_key.scalar() == expected_public_key.scalar()); - } - - #[tokio::test] - async fn private_key_from_env_metadata() { - let private_key = "0x1"; - let env_metadata = dojo_world::metadata::Environment { - private_key: Some(private_key.to_owned()), - ..Default::default() - }; - - let cmd = Command::parse_from(["sozo", "--account-address", "0x0"]); - let result_wallet = cmd.account.signer(Some(&env_metadata)).unwrap(); - let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - FieldElement::from_str(private_key).unwrap(), - )); - - let result_public_key = result_wallet.get_public_key().await.unwrap(); - let expected_public_key = expected_wallet.get_public_key().await.unwrap(); - assert!(result_public_key.scalar() == expected_public_key.scalar()); - } - - #[tokio::test] - async fn keystore_path_and_keystore_password_from_args() { - let keystore_path = "./tests/test_data/keystore/test.json"; - let keystore_password = "dojoftw"; - let private_key = "0x1"; - - let cmd = Command::parse_from([ - "sozo", - "--keystore", - keystore_path, - "--password", - keystore_password, - ]); - let result_wallet = cmd.account.signer(None).unwrap(); - let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - FieldElement::from_str(private_key).unwrap(), - )); - - let result_public_key = result_wallet.get_public_key().await.unwrap(); - let expected_public_key = expected_wallet.get_public_key().await.unwrap(); - assert!(result_public_key.scalar() == expected_public_key.scalar()); - } - - #[tokio::test] - async fn keystore_path_from_env_metadata() { - let keystore_path = "./tests/test_data/keystore/test.json"; - let keystore_password = "dojoftw"; - - let private_key = "0x1"; - let env_metadata = dojo_world::metadata::Environment { - keystore_path: Some(keystore_path.to_owned()), - ..Default::default() - }; - - let cmd = Command::parse_from(["sozo", "--password", keystore_password]); - let result_wallet = cmd.account.signer(Some(&env_metadata)).unwrap(); - let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - FieldElement::from_str(private_key).unwrap(), - )); - - let result_public_key = result_wallet.get_public_key().await.unwrap(); - let expected_public_key = expected_wallet.get_public_key().await.unwrap(); - assert!(result_public_key.scalar() == expected_public_key.scalar()); - } - - #[tokio::test] - async fn keystore_password_from_env_metadata() { - let keystore_path = "./tests/test_data/keystore/test.json"; - let keystore_password = "dojoftw"; - let private_key = "0x1"; - - let env_metadata = dojo_world::metadata::Environment { - keystore_password: Some(keystore_password.to_owned()), - ..Default::default() - }; - - let cmd = Command::parse_from(["sozo", "--keystore", keystore_path]); - let result_wallet = cmd.account.signer(Some(&env_metadata)).unwrap(); - let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - FieldElement::from_str(private_key).unwrap(), - )); - - let result_public_key = result_wallet.get_public_key().await.unwrap(); - let expected_public_key = expected_wallet.get_public_key().await.unwrap(); - assert!(result_public_key.scalar() == expected_public_key.scalar()); - } - - #[test] - fn dont_allow_both_private_key_and_keystore() { - let keystore_path = "./tests/test_data/keystore/test.json"; - let private_key = "0x1"; - let parse_result = Command::try_parse_from([ - "sozo", - "--keystore", - keystore_path, - "--private_key", - private_key, - ]); - assert!(parse_result.is_err()); - } - #[katana_runner::katana_test(2, true, "katana", "")] async fn legacy_flag_works_as_expected() { let cmd = Command::parse_from([ @@ -363,22 +171,4 @@ mod tests { // 0x2 is the Calldata len. assert!(*result.get(3).unwrap() == FieldElement::from_hex_be("0x2").unwrap()); } - - #[test] - fn keystore_path_without_keystore_password() { - let keystore_path = "./tests/test_data/keystore/test.json"; - - let cmd = Command::parse_from(["sozo", "--keystore", keystore_path]); - let result = cmd.account.signer(None); - - assert!(result.is_err()); - } - - #[test] - fn signer_without_pk_or_keystore() { - let cmd = Command::parse_from(["sozo"]); - let result = cmd.account.signer(None); - - assert!(result.is_err()); - } } diff --git a/bin/sozo/src/commands/options/fee.rs b/bin/sozo/src/commands/options/fee.rs new file mode 100644 index 0000000000..7fa13e0826 --- /dev/null +++ b/bin/sozo/src/commands/options/fee.rs @@ -0,0 +1,101 @@ +// MIT License + +// Copyright (c) 2022 Jonathan LEI + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use anyhow::Result; +use bigdecimal::{BigDecimal, Zero}; +use clap::Args; +use num_integer::Integer; +use sozo_ops::account::FeeSetting; +use starknet::macros::felt; +use starknet_crypto::FieldElement; + +#[derive(Debug, Args, Clone)] +#[command(next_help_heading = "Fee options")] +pub struct FeeOptions { + #[clap(long, help = "Maximum transaction fee in Ether (18 decimals)")] + max_fee: Option, + + #[clap(long, help = "Maximum transaction fee in Wei")] + max_fee_raw: Option, + + #[clap(long, help = "Only estimate transaction fee without sending transaction")] + estimate_only: bool, +} + +impl FeeOptions { + pub fn into_setting(self) -> Result { + match (self.max_fee, self.max_fee_raw, self.estimate_only) { + (Some(max_fee), None, false) => { + let max_fee_felt = bigdecimal_to_felt(&max_fee, 18)?; + + // The user is most likely making a mistake for using a max fee higher than 1 ETH + if max_fee_felt > felt!("1000000000000000000") { + anyhow::bail!( + "the --max-fee value is too large. --max-fee expects a value in Ether (18 \ + decimals). Use --max-fee-raw instead to use a raw max_fee amount in Wei." + ) + } + + Ok(FeeSetting::Manual(max_fee_felt)) + } + (None, Some(max_fee_raw), false) => Ok(FeeSetting::Manual(max_fee_raw)), + (None, None, true) => Ok(FeeSetting::EstimateOnly), + (None, None, false) => Ok(FeeSetting::None), + _ => Err(anyhow::anyhow!( + "invalid fee option. At most one of --max-fee, --max-fee-raw, and --estimate-only \ + can be used." + )), + } + } +} + +#[allow(clippy::comparison_chain)] +fn bigdecimal_to_felt(dec: &BigDecimal, decimals: D) -> Result +where + D: Into, +{ + let decimals: i64 = decimals.into(); + + // Scale the bigint part up or down + let (bigint, exponent) = dec.as_bigint_and_exponent(); + + let mut biguint = match bigint.to_biguint() { + Some(value) => value, + None => anyhow::bail!("too many decimal places"), + }; + + if exponent < decimals { + for _ in 0..(decimals - exponent) { + biguint *= 10u32; + } + } else if exponent > decimals { + for _ in 0..(exponent - decimals) { + let (quotient, remainder) = biguint.div_rem(&10u32.into()); + if !remainder.is_zero() { + anyhow::bail!("too many decimal places") + } + biguint = quotient; + } + } + + Ok(FieldElement::from_byte_slice_be(&biguint.to_bytes_be())?) +} diff --git a/bin/sozo/src/commands/options/mod.rs b/bin/sozo/src/commands/options/mod.rs index 0bd599bcc1..9f817439fd 100644 --- a/bin/sozo/src/commands/options/mod.rs +++ b/bin/sozo/src/commands/options/mod.rs @@ -1,4 +1,6 @@ pub mod account; +pub mod fee; +pub mod signer; pub mod starknet; pub mod transaction; pub mod world; diff --git a/bin/sozo/src/commands/options/signer.rs b/bin/sozo/src/commands/options/signer.rs new file mode 100644 index 0000000000..eae8668759 --- /dev/null +++ b/bin/sozo/src/commands/options/signer.rs @@ -0,0 +1,237 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Result}; +use clap::Args; +use dojo_world::metadata::Environment; +use starknet::core::types::FieldElement; +use starknet::signers::{LocalWallet, SigningKey}; + +use super::{DOJO_KEYSTORE_PASSWORD_ENV_VAR, DOJO_KEYSTORE_PATH_ENV_VAR, DOJO_PRIVATE_KEY_ENV_VAR}; + +#[derive(Debug, Args)] +#[command(next_help_heading = "Signer options")] +// INVARIANT: +// - For commandline: we can either specify `private_key` or `keystore_path` along with +// `keystore_password`. This is enforced by Clap. +// - For `Scarb.toml`: if both private_key and keystore are specified in `Scarb.toml` private_key +// will take priority +pub struct SignerOptions { + #[arg(long, env = DOJO_PRIVATE_KEY_ENV_VAR)] + #[arg(conflicts_with = "keystore_path")] + #[arg(help_heading = "Signer options - RAW")] + #[arg(help = "The raw private key associated with the account contract.")] + pub private_key: Option, + + #[arg(long = "keystore", env = DOJO_KEYSTORE_PATH_ENV_VAR)] + #[arg(value_name = "PATH")] + #[arg(help_heading = "Signer options - KEYSTORE")] + #[arg(help = "Use the keystore in the given folder or file.")] + pub keystore_path: Option, + + #[arg(long = "password", env = DOJO_KEYSTORE_PASSWORD_ENV_VAR)] + #[arg(value_name = "PASSWORD")] + #[arg(help_heading = "Signer options - KEYSTORE")] + #[arg(help = "The keystore password. Used with --keystore.")] + pub keystore_password: Option, +} + +impl SignerOptions { + pub fn signer(&self, env_metadata: Option<&Environment>) -> Result { + if let Some(private_key) = + self.private_key.as_deref().or_else(|| env_metadata.and_then(|env| env.private_key())) + { + return Ok(LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + FieldElement::from_str(private_key)?, + ))); + } + + if let Some(path) = &self + .keystore_path + .as_deref() + .or_else(|| env_metadata.and_then(|env| env.keystore_path())) + { + if let Some(password) = self + .keystore_password + .as_deref() + .or_else(|| env_metadata.and_then(|env| env.keystore_password())) + { + return Ok(LocalWallet::from_signing_key(SigningKey::from_keystore( + path, password, + )?)); + } else { + return Err(anyhow!("Keystore path is specified but password is not.")); + } + } + + Err(anyhow!( + "Could not find private key. Please specify the private key or path to the keystore \ + file." + )) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use clap::Parser; + use starknet::signers::{LocalWallet, Signer, SigningKey}; + use starknet_crypto::FieldElement; + + use super::{SignerOptions, DOJO_KEYSTORE_PASSWORD_ENV_VAR, DOJO_PRIVATE_KEY_ENV_VAR}; + + #[derive(clap::Parser, Debug)] + struct Command { + #[clap(flatten)] + pub signer: SignerOptions, + } + + #[test] + fn private_key_read_from_env_variable() { + std::env::set_var(DOJO_PRIVATE_KEY_ENV_VAR, "private_key"); + + let cmd = Command::parse_from(["sozo"]); + assert_eq!(cmd.signer.private_key, Some("private_key".to_owned())); + } + + #[test] + fn keystore_path_read_from_env_variable() { + std::env::set_var(DOJO_KEYSTORE_PASSWORD_ENV_VAR, "keystore_password"); + + let cmd = Command::parse_from(["sozo", "--keystore", "./some/path"]); + assert_eq!(cmd.signer.keystore_password, Some("keystore_password".to_owned())); + } + + #[tokio::test] + async fn private_key_from_args() { + let private_key = "0x1"; + + let cmd = Command::parse_from(["sozo", "--private-key", private_key]); + let result_wallet = cmd.signer.signer(None).unwrap(); + let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + FieldElement::from_str(private_key).unwrap(), + )); + + let result_public_key = result_wallet.get_public_key().await.unwrap(); + let expected_public_key = expected_wallet.get_public_key().await.unwrap(); + assert!(result_public_key.scalar() == expected_public_key.scalar()); + } + + #[tokio::test] + async fn private_key_from_env_metadata() { + let private_key = "0x1"; + let env_metadata = dojo_world::metadata::Environment { + private_key: Some(private_key.to_owned()), + ..Default::default() + }; + + let cmd = Command::parse_from(["sozo"]); + let result_wallet = cmd.signer.signer(Some(&env_metadata)).unwrap(); + let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + FieldElement::from_str(private_key).unwrap(), + )); + + let result_public_key = result_wallet.get_public_key().await.unwrap(); + let expected_public_key = expected_wallet.get_public_key().await.unwrap(); + assert!(result_public_key.scalar() == expected_public_key.scalar()); + } + + #[tokio::test] + async fn keystore_path_and_keystore_password_from_args() { + let keystore_path = "./tests/test_data/keystore/test.json"; + let keystore_password = "dojoftw"; + let private_key = "0x1"; + + let cmd = Command::parse_from([ + "sozo", + "--keystore", + keystore_path, + "--password", + keystore_password, + ]); + let result_wallet = cmd.signer.signer(None).unwrap(); + let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + FieldElement::from_str(private_key).unwrap(), + )); + + let result_public_key = result_wallet.get_public_key().await.unwrap(); + let expected_public_key = expected_wallet.get_public_key().await.unwrap(); + assert!(result_public_key.scalar() == expected_public_key.scalar()); + } + + #[tokio::test] + async fn keystore_path_from_env_metadata() { + let keystore_path = "./tests/test_data/keystore/test.json"; + let keystore_password = "dojoftw"; + + let private_key = "0x1"; + let env_metadata = dojo_world::metadata::Environment { + keystore_path: Some(keystore_path.to_owned()), + ..Default::default() + }; + + let cmd = Command::parse_from(["sozo", "--password", keystore_password]); + let result_wallet = cmd.signer.signer(Some(&env_metadata)).unwrap(); + let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + FieldElement::from_str(private_key).unwrap(), + )); + + let result_public_key = result_wallet.get_public_key().await.unwrap(); + let expected_public_key = expected_wallet.get_public_key().await.unwrap(); + assert!(result_public_key.scalar() == expected_public_key.scalar()); + } + + #[tokio::test] + async fn keystore_password_from_env_metadata() { + let keystore_path = "./tests/test_data/keystore/test.json"; + let keystore_password = "dojoftw"; + let private_key = "0x1"; + + let env_metadata = dojo_world::metadata::Environment { + keystore_password: Some(keystore_password.to_owned()), + ..Default::default() + }; + + let cmd = Command::parse_from(["sozo", "--keystore", keystore_path]); + let result_wallet = cmd.signer.signer(Some(&env_metadata)).unwrap(); + let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + FieldElement::from_str(private_key).unwrap(), + )); + + let result_public_key = result_wallet.get_public_key().await.unwrap(); + let expected_public_key = expected_wallet.get_public_key().await.unwrap(); + assert!(result_public_key.scalar() == expected_public_key.scalar()); + } + + #[test] + fn dont_allow_both_private_key_and_keystore() { + let keystore_path = "./tests/test_data/keystore/test.json"; + let private_key = "0x1"; + let parse_result = Command::try_parse_from([ + "sozo", + "--keystore", + keystore_path, + "--private_key", + private_key, + ]); + assert!(parse_result.is_err()); + } + + #[test] + fn keystore_path_without_keystore_password() { + let keystore_path = "./tests/test_data/keystore/test.json"; + + let cmd = Command::parse_from(["sozo", "--keystore", keystore_path]); + let result = cmd.signer.signer(None); + + assert!(result.is_err()); + } + + #[test] + fn signer_without_pk_or_keystore() { + let cmd = Command::parse_from(["sozo"]); + let result = cmd.signer.signer(None); + + assert!(result.is_err()); + } +} diff --git a/crates/sozo/ops/Cargo.toml b/crates/sozo/ops/Cargo.toml index 2472bb87fe..4ce5d81198 100644 --- a/crates/sozo/ops/Cargo.toml +++ b/crates/sozo/ops/Cargo.toml @@ -8,6 +8,7 @@ version.workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true +cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.2" } cairo-lang-compiler.workspace = true cairo-lang-defs.workspace = true cairo-lang-filesystem.workspace = true @@ -23,6 +24,8 @@ camino.workspace = true clap-verbosity-flag = "2.0.1" clap.workspace = true clap_complete.workspace = true +colored = "2.0.0" +colored_json = "3.2.0" console.workspace = true dojo-bindgen.workspace = true dojo-lang.workspace = true @@ -31,11 +34,13 @@ dojo-world = { workspace = true, features = [ "contracts", "metadata", "migratio futures.workspace = true notify = "6.0.1" notify-debouncer-mini = "0.3.0" +rpassword = "7.2.0" scarb-ui.workspace = true scarb.workspace = true semver.workspace = true serde.workspace = true serde_json.workspace = true +serde_with.workspace = true smol_str.workspace = true starknet-crypto.workspace = true starknet.workspace = true @@ -44,7 +49,6 @@ tokio.workspace = true tracing-log = "0.1.3" tracing.workspace = true url.workspace = true -cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.2" } [dev-dependencies] assert_fs = "1.0.10" diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs new file mode 100644 index 0000000000..205f14ded0 --- /dev/null +++ b/crates/sozo/ops/src/account.rs @@ -0,0 +1,424 @@ +// MIT License + +// Copyright (c) 2022 Jonathan LEI + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use std::io::Write; +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::Result; +use colored::Colorize; +use colored_json::{ColorMode, Output}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use starknet::accounts::{AccountFactory, AccountFactoryError, OpenZeppelinAccountFactory}; +use starknet::core::serde::unsigned_field_element::UfeHex; +use starknet::core::types::{BlockId, BlockTag, ExecutionResult, StarknetError}; +use starknet::core::utils::get_contract_address; +use starknet::macros::felt; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider, ProviderError}; +use starknet::signers::{LocalWallet, Signer, SigningKey}; +use starknet_crypto::FieldElement; + +// use crate::commands::account::AccountCommand; +// use crate::commands::options::fee::FeeSetting; + +/// The canonical hash of a contract class. This is the class hash value of a contract instance. +pub type ClassHash = FieldElement; + +/// The class hash of DEFAULT_OZ_ACCOUNT_CONTRACT. +/// Corresponds to 0x05400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c +pub const DEFAULT_OZ_ACCOUNT_CONTRACT_CLASS_HASH: ClassHash = FieldElement::from_mont([ + 8460675502047588988, + 17729791148444280953, + 7171298771336181387, + 292243705759714441, +]); + +#[derive(Serialize, Deserialize)] +pub struct AccountConfig { + pub version: u64, + pub variant: AccountVariant, + pub deployment: DeploymentStatus, +} + +impl AccountConfig { + pub fn deploy_account_address(&self) -> Result { + let undeployed_status = match &self.deployment { + DeploymentStatus::Undeployed(value) => value, + DeploymentStatus::Deployed(_) => { + anyhow::bail!("account already deployed"); + } + }; + + match &self.variant { + AccountVariant::OpenZeppelin(oz) => Ok(get_contract_address( + undeployed_status.salt, + undeployed_status.class_hash, + &[oz.public_key], + FieldElement::ZERO, + )), + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AccountVariant { + OpenZeppelin(OzAccountConfig), +} + +#[serde_as] +#[derive(Serialize, Deserialize)] +pub struct OzAccountConfig { + pub version: u64, + #[serde_as(as = "UfeHex")] + pub public_key: FieldElement, + #[serde(default = "true_as_default")] + pub legacy: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum DeploymentStatus { + Undeployed(UndeployedStatus), + Deployed(DeployedStatus), +} + +#[serde_as] +#[derive(Serialize, Deserialize)] +pub struct UndeployedStatus { + #[serde_as(as = "UfeHex")] + pub class_hash: FieldElement, + #[serde_as(as = "UfeHex")] + pub salt: FieldElement, +} + +#[serde_as] +#[derive(Serialize, Deserialize)] +pub struct DeployedStatus { + #[serde_as(as = "UfeHex")] + pub class_hash: FieldElement, + #[serde_as(as = "UfeHex")] + pub address: FieldElement, +} + +enum MaxFeeType { + Manual { max_fee: FieldElement }, + Estimated { estimate: FieldElement, estimate_with_buffer: FieldElement }, +} + +impl MaxFeeType { + pub fn max_fee(&self) -> FieldElement { + match self { + Self::Manual { max_fee } => *max_fee, + Self::Estimated { estimate_with_buffer, .. } => *estimate_with_buffer, + } + } +} + +pub async fn new(signer: LocalWallet, force: bool, output: PathBuf) -> Result<()> { + if output.exists() && !force { + anyhow::bail!("account config file already exists"); + } + + let salt = SigningKey::from_random().secret_scalar(); + + let account_config = AccountConfig { + version: 1, + variant: AccountVariant::OpenZeppelin(OzAccountConfig { + version: 1, + public_key: signer.get_public_key().await?.scalar(), + legacy: false, + }), + deployment: DeploymentStatus::Undeployed(UndeployedStatus { + class_hash: DEFAULT_OZ_ACCOUNT_CONTRACT_CLASS_HASH, + salt, + }), + }; + + let deployed_address = account_config.deploy_account_address()?; + + let mut file = std::fs::File::create(&output)?; + serde_json::to_writer_pretty(&mut file, &account_config)?; + file.write_all(b"\n")?; + + eprintln!("Created new account config file: {}", std::fs::canonicalize(&output)?.display()); + eprintln!(); + eprintln!( + "Once deployed, this account will be available at:\n {}", + format!("{:#064x}", deployed_address).bright_yellow() + ); + eprintln!(); + eprintln!( + "Deploy this account by running:\n {}", + format!("sozo account deploy {}", output.display()).bright_yellow() + ); + + Ok(()) +} + +pub async fn deploy( + provider: JsonRpcClient, + signer: LocalWallet, + fee_setting: FeeSetting, + simulate: bool, + nonce: Option, + poll_interval: u64, + file: PathBuf, +) -> Result<()> { + if simulate && fee_setting.is_estimate_only() { + anyhow::bail!("--simulate cannot be used with --estimate-only"); + } + + if !file.exists() { + anyhow::bail!("account config file not found"); + } + + let mut account: AccountConfig = serde_json::from_reader(&mut std::fs::File::open(&file)?)?; + + let signer_public_key = signer.get_public_key().await?.scalar(); + + let undeployed_status = match &account.deployment { + DeploymentStatus::Undeployed(inner) => inner, + DeploymentStatus::Deployed(_) => { + anyhow::bail!("account already deployed"); + } + }; + + let chain_id = provider.chain_id().await?; + + let factory = match &account.variant { + AccountVariant::OpenZeppelin(oz_config) => { + // Makes sure we're using the right key + if signer_public_key != oz_config.public_key { + anyhow::bail!( + "public key mismatch. Expected: {:#064x}; actual: {:#064x}.", + oz_config.public_key, + signer_public_key + ); + } + + let mut factory = OpenZeppelinAccountFactory::new( + undeployed_status.class_hash, + chain_id, + signer, + &provider, + ) + .await?; + factory.set_block_id(BlockId::Tag(BlockTag::Pending)); + + factory + } + }; + + let account_deployment = factory.deploy(undeployed_status.salt); + + let target_deployment_address = account.deploy_account_address()?; + + // Sanity check. We don't really need to check again here actually + if account_deployment.address() != target_deployment_address { + panic!("Unexpected account deployment address mismatch"); + } + + let max_fee = match fee_setting { + FeeSetting::Manual(fee) => MaxFeeType::Manual { max_fee: fee }, + FeeSetting::EstimateOnly | FeeSetting::None => { + let estimated_fee = account_deployment + .estimate_fee() + .await + .map_err(|err| match err { + AccountFactoryError::Provider(ProviderError::StarknetError(err)) => { + map_starknet_error(err) + } + err => anyhow::anyhow!("{}", err), + })? + .overall_fee; + + let estimated_fee_with_buffer = (estimated_fee * felt!("3")).floor_div(felt!("2")); + + if fee_setting.is_estimate_only() { + println!("{} ETH", format!("{}", estimated_fee.to_big_decimal(18)).bright_yellow(),); + return Ok(()); + } + + MaxFeeType::Estimated { + estimate: estimated_fee, + estimate_with_buffer: estimated_fee_with_buffer, + } + } + }; + + if !simulate { + match max_fee { + MaxFeeType::Manual { max_fee } => { + eprintln!( + "You've manually specified the account deployment fee to be {}. Therefore, \ + fund at least:\n {}", + format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), + format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), + ); + } + MaxFeeType::Estimated { estimate, estimate_with_buffer } => { + eprintln!( + "The estimated account deployment fee is {}. However, to avoid failure, fund \ + at least:\n {}", + format!("{} ETH", estimate.to_big_decimal(18)).bright_yellow(), + format!("{} ETH", estimate_with_buffer.to_big_decimal(18)).bright_yellow() + ); + } + } + + eprintln!( + "to the following address:\n {}", + format!("{:#064x}", target_deployment_address).bright_yellow() + ); + + eprint!("Press [ENTER] once you've funded the address."); + std::io::stdin().read_line(&mut String::new())?; + } + + let account_deployment = match nonce { + Some(nonce) => account_deployment.nonce(nonce), + None => account_deployment, + }; + let account_deployment = account_deployment.max_fee(max_fee.max_fee()); + + if simulate { + let simulation = account_deployment.simulate(false, false).await?; + let simulation_json = serde_json::to_value(simulation)?; + + let simulation_json = + colored_json::to_colored_json(&simulation_json, ColorMode::Auto(Output::StdOut))?; + println!("{simulation_json}"); + return Ok(()); + } + + let account_deployment_tx = account_deployment.send().await?.transaction_hash; + eprintln!( + "Account deployment transaction: {}", + format!("{:#064x}", account_deployment_tx).bright_yellow() + ); + + // By default we wait for the tx to confirm so that we don't incorrectly mark the + // account as deployed + eprintln!( + "Waiting for transaction {} to confirm. If this process is interrupted, you will need to \ + run `{}` to update the account file.", + format!("{:#064x}", account_deployment_tx).bright_yellow(), + "sozo account fetch".bright_yellow(), + ); + watch_tx(&provider, account_deployment_tx, Duration::from_millis(poll_interval)).await?; + + account.deployment = DeploymentStatus::Deployed(DeployedStatus { + class_hash: undeployed_status.class_hash, + address: target_deployment_address, + }); + + // Never write directly to the original file to avoid data loss + let mut temp_file_name = file + .file_name() + .ok_or_else(|| anyhow::anyhow!("unable to determine file name"))? + .to_owned(); + temp_file_name.push(".tmp"); + let mut temp_path = file.clone(); + temp_path.set_file_name(temp_file_name); + + let mut temp_file = std::fs::File::create(&temp_path)?; + serde_json::to_writer_pretty(&mut temp_file, &account)?; + temp_file.write_all(b"\n")?; + std::fs::rename(temp_path, file)?; + + Ok(()) +} + +fn true_as_default() -> bool { + true +} + +fn map_starknet_error(err: StarknetError) -> anyhow::Error { + match err { + StarknetError::ContractError(err) => { + anyhow::anyhow!("ContractError: {}", err.revert_error.trim()) + } + StarknetError::TransactionExecutionError(err) => { + anyhow::anyhow!( + "TransactionExecutionError (tx index {}): {}", + err.transaction_index, + err.execution_error.trim() + ) + } + StarknetError::ValidationFailure(err) => { + anyhow::anyhow!("ValidationFailure: {}", err.trim()) + } + StarknetError::UnexpectedError(err) => { + anyhow::anyhow!("UnexpectedError: {}", err.trim()) + } + err => anyhow::anyhow!("{}", err), + } +} + +pub async fn watch_tx

( + provider: P, + transaction_hash: FieldElement, + poll_interval: Duration, +) -> Result<()> +where + P: Provider, +{ + loop { + match provider.get_transaction_receipt(transaction_hash).await { + Ok(receipt) => match receipt.execution_result() { + ExecutionResult::Succeeded => { + eprintln!( + "Transaction {} confirmed", + format!("{:#064x}", transaction_hash).bright_yellow() + ); + + return Ok(()); + } + ExecutionResult::Reverted { reason } => { + return Err(anyhow::anyhow!("transaction reverted: {}", reason)); + } + }, + Err(ProviderError::StarknetError(StarknetError::TransactionHashNotFound)) => { + eprintln!("Transaction not confirmed yet..."); + } + Err(err) => return Err(err.into()), + } + + tokio::time::sleep(poll_interval).await; + } +} + +#[derive(Debug)] +pub enum FeeSetting { + Manual(FieldElement), + EstimateOnly, + None, +} + +impl FeeSetting { + pub fn is_estimate_only(&self) -> bool { + matches!(self, FeeSetting::EstimateOnly) + } +} diff --git a/crates/sozo/ops/src/keystore.rs b/crates/sozo/ops/src/keystore.rs new file mode 100644 index 0000000000..3ff7d6df57 --- /dev/null +++ b/crates/sozo/ops/src/keystore.rs @@ -0,0 +1,140 @@ +// MIT License + +// Copyright (c) 2022 Jonathan LEI + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use std::io::Read; +use std::path::PathBuf; + +use anyhow::Result; +use colored::Colorize; +use starknet::signers::SigningKey; +use starknet_crypto::FieldElement; + +const RAW_PASSWORD_WARNING: &str = "WARNING: setting passwords via --password is generally \ + considered insecure, as they will be stored in your shell \ + history or other log files."; + +pub fn new(password: Option, force: bool, file: PathBuf) -> Result<()> { + if password.is_some() { + eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); + } + + if file.exists() && !force { + anyhow::bail!("keystore file already exists"); + } + + let password = get_password(password)?; + + let key = SigningKey::from_random(); + key.save_as_keystore(&file, &password)?; + + println!("Created new encrypted keystore file: {}", std::fs::canonicalize(file)?.display()); + println!("Public key: {}", format!("{:#064x}", key.verifying_key().scalar()).bright_yellow()); + + Ok(()) +} + +pub fn from_key( + force: bool, + private_key_stdin: bool, + password: Option, + file: PathBuf, +) -> Result<()> { + if password.is_some() { + eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); + } + + if file.exists() && !force { + anyhow::bail!("keystore file already exists"); + } + + let private_key = if private_key_stdin { + let mut buffer = String::new(); + std::io::stdin().read_to_string(&mut buffer)?; + + buffer + } else { + rpassword::prompt_password("Enter private key: ")? + }; + let private_key = FieldElement::from_hex_be(private_key.trim())?; + + let password = get_password(password)?; + + let key = SigningKey::from_secret_scalar(private_key); + key.save_as_keystore(&file, &password)?; + + println!("Created new encrypted keystore file: {}", std::fs::canonicalize(file)?.display()); + println!("Public key: {:#064x}", key.verifying_key().scalar()); + + Ok(()) +} + +pub fn inspect(password: Option, raw: bool, file: PathBuf) -> Result<()> { + if password.is_some() { + eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); + } + + if !file.exists() { + anyhow::bail!("keystore file not found"); + } + + let password = get_password(password)?; + + let key = SigningKey::from_keystore(file, &password)?; + + if raw { + println!("{:#064x}", key.verifying_key().scalar()); + } else { + println!("Public key: {:#064x}", key.verifying_key().scalar()); + } + + Ok(()) +} + +pub fn inspect_private(password: Option, raw: bool, file: PathBuf) -> Result<()> { + if password.is_some() { + eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); + } + + if !file.exists() { + anyhow::bail!("keystore file not found"); + } + + let password = get_password(password)?; + + let key = SigningKey::from_keystore(file, &password)?; + + if raw { + println!("{:#064x}", key.secret_scalar()); + } else { + println!("Private key: {:#064x}", key.secret_scalar()); + } + + Ok(()) +} + +fn get_password(password: Option) -> std::io::Result { + if let Some(password) = password { + Ok(password) + } else { + rpassword::prompt_password("Enter password: ") + } +} diff --git a/crates/sozo/ops/src/lib.rs b/crates/sozo/ops/src/lib.rs index 676e8e86b8..acab8a6808 100644 --- a/crates/sozo/ops/src/lib.rs +++ b/crates/sozo/ops/src/lib.rs @@ -4,9 +4,11 @@ use dojo_world::migration::strategy::generate_salt; use starknet::accounts::ConnectedAccount; use starknet::core::types::FieldElement; +pub mod account; pub mod auth; pub mod events; pub mod execute; +pub mod keystore; pub mod migration; pub mod model; pub mod register; From 5b27b62aad862350f75733a98d304cffabb5c6fe Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 02:54:32 +0100 Subject: [PATCH 02/18] STARKNET_POLL_INTERVAL default 5000 -> 1000 --- bin/sozo/src/commands/account.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs index e2c23e7401..e353f3d682 100644 --- a/bin/sozo/src/commands/account.rs +++ b/bin/sozo/src/commands/account.rs @@ -75,7 +75,7 @@ pub enum AccountCommand { #[clap( long, env = "STARKNET_POLL_INTERVAL", - default_value = "5000", + default_value = "1000", help = "Transaction result poll interval in milliseconds" )] poll_interval: u64, From 44f4cb21289cf9143487cb2766adf839d4e21731 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 03:06:13 +0100 Subject: [PATCH 03/18] Prompt password instead of error --- Cargo.lock | 1 + Cargo.toml | 5 +++-- bin/sozo/Cargo.toml | 1 + bin/sozo/src/commands/options/signer.rs | 24 +++++++++++++----------- crates/sozo/ops/Cargo.toml | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34d1ad10cb..2ef4f58dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11153,6 +11153,7 @@ dependencies = [ "notify-debouncer-mini", "num-bigint", "num-integer", + "rpassword", "scarb", "scarb-ui", "semver 1.0.21", diff --git a/Cargo.toml b/Cargo.toml index bc5f44e2e9..991ee50676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,8 +91,8 @@ torii-server = { path = "crates/torii/server" } saya-core = { path = "crates/saya/core" } # sozo -sozo-signers = { path = "crates/sozo/signers" } sozo-ops = { path = "crates/sozo/ops" } +sozo-signers = { path = "crates/sozo/signers" } anyhow = "1.0.75" assert_matches = "1.5.0" @@ -142,6 +142,8 @@ once_cell = "1.0" parking_lot = "0.12.1" pretty_assertions = "1.2.1" rayon = "1.8.0" +regex = "1.10.3" +rpassword = "7.2.0" salsa = "0.16.1" scarb = { git = "https://github.com/software-mansion/scarb", tag = "v2.5.4" } scarb-ui = { git = "https://github.com/software-mansion/scarb", tag = "v2.5.4" } @@ -163,7 +165,6 @@ tokio = { version = "1.32.0", features = [ "full" ] } toml = "0.7.4" tracing = "0.1.34" tracing-subscriber = { version = "0.3.16", features = [ "env-filter", "json" ] } -regex = "1.10.3" url = { version = "2.4.0", features = [ "serde" ] } # server diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index cea62c5119..fcb2de2de8 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -34,6 +34,7 @@ notify = "6.0.1" notify-debouncer-mini = "0.3.0" num-bigint = "0.4.3" num-integer = "0.1.45" +rpassword.workspace = true scarb-ui.workspace = true scarb.workspace = true semver.workspace = true diff --git a/bin/sozo/src/commands/options/signer.rs b/bin/sozo/src/commands/options/signer.rs index eae8668759..311674d5d4 100644 --- a/bin/sozo/src/commands/options/signer.rs +++ b/bin/sozo/src/commands/options/signer.rs @@ -50,17 +50,19 @@ impl SignerOptions { .as_deref() .or_else(|| env_metadata.and_then(|env| env.keystore_path())) { - if let Some(password) = self - .keystore_password - .as_deref() - .or_else(|| env_metadata.and_then(|env| env.keystore_password())) - { - return Ok(LocalWallet::from_signing_key(SigningKey::from_keystore( - path, password, - )?)); - } else { - return Err(anyhow!("Keystore path is specified but password is not.")); - } + let password = { + if let Some(password) = self + .keystore_password + .as_deref() + .or_else(|| env_metadata.and_then(|env| env.keystore_password())) + { + password.to_owned() + } else { + rpassword::prompt_password("Enter password: ")? + } + }; + let private_key = SigningKey::from_keystore(path, &password)?; + return Ok(LocalWallet::from_signing_key(private_key)); } Err(anyhow!( diff --git a/crates/sozo/ops/Cargo.toml b/crates/sozo/ops/Cargo.toml index 4ce5d81198..15bd2435c4 100644 --- a/crates/sozo/ops/Cargo.toml +++ b/crates/sozo/ops/Cargo.toml @@ -34,7 +34,7 @@ dojo-world = { workspace = true, features = [ "contracts", "metadata", "migratio futures.workspace = true notify = "6.0.1" notify-debouncer-mini = "0.3.0" -rpassword = "7.2.0" +rpassword.workspace = true scarb-ui.workspace = true scarb.workspace = true semver.workspace = true From 75e188a5e04e7abbfc459f892f4c4e66e9f28bbc Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 03:06:56 +0100 Subject: [PATCH 04/18] Remove num-bigint dependency --- Cargo.lock | 1 - bin/sozo/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ef4f58dd1..ef13aa39f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11151,7 +11151,6 @@ dependencies = [ "katana-runner", "notify", "notify-debouncer-mini", - "num-bigint", "num-integer", "rpassword", "scarb", diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index fcb2de2de8..1615ffe48d 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -32,7 +32,6 @@ dojo-world = { workspace = true, features = [ "contracts", "metadata", "migratio futures.workspace = true notify = "6.0.1" notify-debouncer-mini = "0.3.0" -num-bigint = "0.4.3" num-integer = "0.1.45" rpassword.workspace = true scarb-ui.workspace = true From bb7eca67c65662595aacec2a64b86cf6ab6c091c Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 03:09:51 +0100 Subject: [PATCH 05/18] Mention starkli license in sozo README --- bin/sozo/README.md | 3 +++ bin/sozo/src/commands/account.rs | 22 ---------------------- bin/sozo/src/commands/keystore.rs | 22 ---------------------- bin/sozo/src/commands/options/fee.rs | 22 ---------------------- crates/sozo/ops/src/account.rs | 22 ---------------------- crates/sozo/ops/src/keystore.rs | 22 ---------------------- 6 files changed, 3 insertions(+), 110 deletions(-) diff --git a/bin/sozo/README.md b/bin/sozo/README.md index 5c465a1296..c88c71a7e2 100644 --- a/bin/sozo/README.md +++ b/bin/sozo/README.md @@ -5,3 +5,6 @@ curl -L https://install.dojoengine.org | bash ``` [Documentation](https://book.dojoengine.org/toolchain/sozo/overview) + +Some parts of sozo were inspired by [starkli](https://github.com/xJonathanLEI/starkli) created by Jonathan LEI. +It is licensed under Apache 2.0 / MIT License. diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs index e353f3d682..906af2eb8f 100644 --- a/bin/sozo/src/commands/account.rs +++ b/bin/sozo/src/commands/account.rs @@ -1,25 +1,3 @@ -// MIT License - -// Copyright (c) 2022 Jonathan LEI - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - use std::path::PathBuf; use anyhow::Result; diff --git a/bin/sozo/src/commands/keystore.rs b/bin/sozo/src/commands/keystore.rs index a04be6f5c7..fab0eb4bf6 100644 --- a/bin/sozo/src/commands/keystore.rs +++ b/bin/sozo/src/commands/keystore.rs @@ -1,25 +1,3 @@ -// MIT License - -// Copyright (c) 2022 Jonathan LEI - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - use std::path::PathBuf; use anyhow::Result; diff --git a/bin/sozo/src/commands/options/fee.rs b/bin/sozo/src/commands/options/fee.rs index 7fa13e0826..bee0a5246b 100644 --- a/bin/sozo/src/commands/options/fee.rs +++ b/bin/sozo/src/commands/options/fee.rs @@ -1,25 +1,3 @@ -// MIT License - -// Copyright (c) 2022 Jonathan LEI - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - use anyhow::Result; use bigdecimal::{BigDecimal, Zero}; use clap::Args; diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index 205f14ded0..c22abcb988 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -1,25 +1,3 @@ -// MIT License - -// Copyright (c) 2022 Jonathan LEI - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - use std::io::Write; use std::path::PathBuf; use std::time::Duration; diff --git a/crates/sozo/ops/src/keystore.rs b/crates/sozo/ops/src/keystore.rs index 3ff7d6df57..6ab29f52fa 100644 --- a/crates/sozo/ops/src/keystore.rs +++ b/crates/sozo/ops/src/keystore.rs @@ -1,25 +1,3 @@ -// MIT License - -// Copyright (c) 2022 Jonathan LEI - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - use std::io::Read; use std::path::PathBuf; From d3134351c07e0a0dbd424e7e5a7fe8ee096c02e6 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 03:10:39 +0100 Subject: [PATCH 06/18] Remove dead code --- crates/sozo/ops/src/account.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index c22abcb988..18db493546 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -17,9 +17,6 @@ use starknet::providers::{JsonRpcClient, Provider, ProviderError}; use starknet::signers::{LocalWallet, Signer, SigningKey}; use starknet_crypto::FieldElement; -// use crate::commands::account::AccountCommand; -// use crate::commands::options::fee::FeeSetting; - /// The canonical hash of a contract class. This is the class hash value of a contract instance. pub type ClassHash = FieldElement; From 43a1f6e9e9f793a9f9f9170c03ed60c9125babd4 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 03:27:09 +0100 Subject: [PATCH 07/18] Add dojo-utils crate --- Cargo.lock | 17 ++++- Cargo.toml | 70 ++++++++++--------- bin/sozo/Cargo.toml | 1 + crates/dojo-utils/Cargo.toml | 19 +++++ .../src/utils.rs => dojo-utils/src/lib.rs} | 0 crates/dojo-world/Cargo.toml | 5 +- crates/dojo-world/src/lib.rs | 2 - crates/dojo-world/src/migration/mod.rs | 3 +- crates/sozo/ops/Cargo.toml | 1 + crates/sozo/ops/src/migration/mod.rs | 2 +- crates/sozo/ops/src/utils.rs | 2 +- crates/torii/graphql/Cargo.toml | 7 +- crates/torii/graphql/src/tests/mod.rs | 2 +- 13 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 crates/dojo-utils/Cargo.toml rename crates/{dojo-world/src/utils.rs => dojo-utils/src/lib.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index ef13aa39f4..b33d983ed4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3392,6 +3392,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dojo-utils" +version = "0.6.0-alpha.7" +dependencies = [ + "assert_matches", + "dojo-test-utils", + "futures", + "starknet 0.9.0", + "thiserror", + "tokio", +] + [[package]] name = "dojo-world" version = "0.6.0-alpha.7" @@ -3409,7 +3421,7 @@ dependencies = [ "dojo-lang", "dojo-test-utils", "dojo-types", - "futures", + "dojo-utils", "http", "ipfs-api-backend-hyper", "scarb", @@ -11146,6 +11158,7 @@ dependencies = [ "dojo-lang", "dojo-test-utils", "dojo-types", + "dojo-utils", "dojo-world", "futures", "katana-runner", @@ -11200,6 +11213,7 @@ dependencies = [ "dojo-lang", "dojo-test-utils", "dojo-types", + "dojo-utils", "dojo-world", "futures", "katana-runner", @@ -12660,6 +12674,7 @@ dependencies = [ "convert_case 0.6.0", "dojo-test-utils", "dojo-types", + "dojo-utils", "dojo-world", "lazy_static", "regex", diff --git a/Cargo.toml b/Cargo.toml index 991ee50676..134d4d39be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,40 +2,41 @@ resolver = "2" members = [ - "bin/dojo-language-server", - "bin/katana", - "bin/saya", - "bin/sozo", - "bin/torii", - "crates/benches", - "crates/common", - "crates/dojo-bindgen", - "crates/dojo-core", - "crates/dojo-lang", - "crates/dojo-test-utils", - "crates/dojo-types", - "crates/dojo-world", - "crates/dojo-world/abigen", - "crates/katana/core", - "crates/katana/executor", - "crates/katana/primitives", - "crates/katana/rpc/rpc", - "crates/katana/rpc/rpc-api", - "crates/katana/rpc/rpc-types", - "crates/katana/rpc/rpc-types-builder", - "crates/katana/runner", - "crates/katana/runner/runner-macro", - "crates/katana/storage/codecs", - "crates/katana/storage/codecs/derive", - "crates/katana/storage/db", - "crates/katana/storage/provider", - "crates/metrics", - "crates/saya/core", - "crates/sozo/signers", - "crates/torii/client", - "crates/torii/server", - "crates/torii/types-test", - "examples/spawn-and-move", + "bin/dojo-language-server", + "bin/katana", + "bin/saya", + "bin/sozo", + "bin/torii", + "crates/benches", + "crates/common", + "crates/dojo-bindgen", + "crates/dojo-core", + "crates/dojo-lang", + "crates/dojo-test-utils", + "crates/dojo-types", + "crates/dojo-utils", + "crates/dojo-world", + "crates/dojo-world/abigen", + "crates/katana/core", + "crates/katana/executor", + "crates/katana/primitives", + "crates/katana/rpc/rpc", + "crates/katana/rpc/rpc-api", + "crates/katana/rpc/rpc-types", + "crates/katana/rpc/rpc-types-builder", + "crates/katana/runner", + "crates/katana/runner/runner-macro", + "crates/katana/storage/codecs", + "crates/katana/storage/codecs/derive", + "crates/katana/storage/db", + "crates/katana/storage/provider", + "crates/metrics", + "crates/saya/core", + "crates/sozo/signers", + "crates/torii/client", + "crates/torii/server", + "crates/torii/types-test", + "examples/spawn-and-move", ] [workspace.package] @@ -63,6 +64,7 @@ dojo-core = { path = "crates/dojo-core" } dojo-lang = { path = "crates/dojo-lang" } dojo-test-utils = { path = "crates/dojo-test-utils" } dojo-types = { path = "crates/dojo-types" } +dojo-utils = { path = "crates/dojo-utils" } dojo-world = { path = "crates/dojo-world" } # katana diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index 1615ffe48d..429d8731f6 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -28,6 +28,7 @@ console.workspace = true dojo-bindgen.workspace = true dojo-lang.workspace = true dojo-types.workspace = true +dojo-utils.workspace = true dojo-world = { workspace = true, features = [ "contracts", "metadata", "migration" ] } futures.workspace = true notify = "6.0.1" diff --git a/crates/dojo-utils/Cargo.toml b/crates/dojo-utils/Cargo.toml new file mode 100644 index 0000000000..8d599ab558 --- /dev/null +++ b/crates/dojo-utils/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition.workspace = true +license-file.workspace = true +license.workspace = true +name = "dojo-utils" +repository.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures.workspace = true +starknet.workspace = true +thiserror.workspace = true +tokio = { version = "1.32.0", features = [ "time" ], default-features = false } + +[dev-dependencies] +assert_matches.workspace = true +dojo-test-utils.workspace = true diff --git a/crates/dojo-world/src/utils.rs b/crates/dojo-utils/src/lib.rs similarity index 100% rename from crates/dojo-world/src/utils.rs rename to crates/dojo-utils/src/lib.rs diff --git a/crates/dojo-world/Cargo.toml b/crates/dojo-world/Cargo.toml index 728721454c..be6e241803 100644 --- a/crates/dojo-world/Cargo.toml +++ b/crates/dojo-world/Cargo.toml @@ -7,6 +7,8 @@ repository.workspace = true version.workspace = true [dependencies] +dojo-utils.workspace = true + anyhow.workspace = true async-trait.workspace = true cairo-lang-filesystem.workspace = true @@ -14,7 +16,6 @@ cairo-lang-project.workspace = true cairo-lang-starknet.workspace = true camino.workspace = true convert_case.workspace = true -futures.workspace = true serde.workspace = true serde_json.workspace = true serde_with.workspace = true @@ -45,4 +46,4 @@ tokio.workspace = true contracts = [ "dep:dojo-types", "dep:http" ] manifest = [ "contracts", "dep:dojo-types", "dep:url" ] metadata = [ "dep:ipfs-api-backend-hyper", "dep:scarb", "dep:url" ] -migration = [ "dep:tokio" ] +migration = [ ] diff --git a/crates/dojo-world/src/lib.rs b/crates/dojo-world/src/lib.rs index 2fb2072b76..e9299cd509 100644 --- a/crates/dojo-world/src/lib.rs +++ b/crates/dojo-world/src/lib.rs @@ -6,5 +6,3 @@ pub mod manifest; pub mod metadata; #[cfg(feature = "migration")] pub mod migration; -#[cfg(feature = "migration")] -pub mod utils; // TODO: move to somewhere else diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index bafb4d25dc..0d5fb085c8 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -6,6 +6,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use cairo_lang_starknet::casm_contract_class::CasmContractClass; use cairo_lang_starknet::contract_class::ContractClass; +use dojo_utils::{TransactionWaiter, TransactionWaitingError}; use starknet::accounts::{Account, AccountError, Call, ConnectedAccount, SingleOwnerAccount}; use starknet::core::types::contract::{CompiledClass, SierraClass}; use starknet::core::types::{ @@ -20,8 +21,6 @@ use starknet::providers::{Provider, ProviderError}; use starknet::signers::Signer; use thiserror::Error; -use crate::utils::{TransactionWaiter, TransactionWaitingError}; - pub mod class; pub mod contract; pub mod strategy; diff --git a/crates/sozo/ops/Cargo.toml b/crates/sozo/ops/Cargo.toml index 15bd2435c4..58c67eba87 100644 --- a/crates/sozo/ops/Cargo.toml +++ b/crates/sozo/ops/Cargo.toml @@ -30,6 +30,7 @@ console.workspace = true dojo-bindgen.workspace = true dojo-lang.workspace = true dojo-types.workspace = true +dojo-utils.workspace = true dojo-world = { workspace = true, features = [ "contracts", "metadata", "migration" ] } futures.workspace = true notify = "6.0.1" diff --git a/crates/sozo/ops/src/migration/mod.rs b/crates/sozo/ops/src/migration/mod.rs index 781d330dcb..8f6b499cfc 100644 --- a/crates/sozo/ops/src/migration/mod.rs +++ b/crates/sozo/ops/src/migration/mod.rs @@ -3,6 +3,7 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; use camino::Utf8PathBuf; use dojo_lang::compiler::{ABIS_DIR, BASE_DIR, DEPLOYMENTS_DIR, MANIFESTS_DIR, OVERLAYS_DIR}; +use dojo_utils::TransactionWaiter; use dojo_world::contracts::abi::world::ResourceMetadata; use dojo_world::contracts::cairo_utils; use dojo_world::contracts::world::WorldContract; @@ -17,7 +18,6 @@ use dojo_world::migration::world::WorldDiff; use dojo_world::migration::{ Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, TxConfig, }; -use dojo_world::utils::TransactionWaiter; use scarb::core::Workspace; use scarb_ui::Ui; use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; diff --git a/crates/sozo/ops/src/utils.rs b/crates/sozo/ops/src/utils.rs index a2aaf99f96..65d3391c49 100644 --- a/crates/sozo/ops/src/utils.rs +++ b/crates/sozo/ops/src/utils.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use dojo_world::utils::{execution_status_from_maybe_pending_receipt, TransactionWaiter}; +use dojo_utils::{execution_status_from_maybe_pending_receipt, TransactionWaiter}; use starknet::core::types::{ExecutionResult, InvokeTransactionResult}; use starknet::providers::Provider; diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index d984c87047..4598f92288 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -17,7 +17,8 @@ async-trait.workspace = true base64.workspace = true chrono.workspace = true convert_case = "0.6.0" -dojo-types = { path = "../../dojo-types" } +dojo-types.workspace = true +dojo-utils.workspace = true lazy_static.workspace = true scarb-ui.workspace = true serde.workspace = true @@ -39,8 +40,8 @@ sozo-ops.workspace = true [dev-dependencies] camino.workspace = true -dojo-test-utils = { path = "../../dojo-test-utils", features = [ "build-examples" ] } -dojo-world = { path = "../../dojo-world" } +dojo-test-utils = { workspace = true, features = [ "build-examples" ] } +dojo-world.workspace = true scarb.workspace = true serial_test = "2.0.0" sozo = { path = "../../../bin/sozo" } diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index a96c72d240..a48e2e0228 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -9,9 +9,9 @@ use dojo_test_utils::sequencer::{ }; use dojo_types::primitive::Primitive; use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; +use dojo_utils::TransactionWaiter; use dojo_world::contracts::WorldContractReader; use dojo_world::manifest::DeploymentManifest; -use dojo_world::utils::TransactionWaiter; use scarb::ops; use serde::Deserialize; use serde_json::Value; From 1580b16a314b2e29be2ac330624778434ef7d4b1 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Fri, 22 Mar 2024 23:54:49 +0100 Subject: [PATCH 08/18] Revert "Add dojo-utils crate" This reverts commit 43a1f6e9e9f793a9f9f9170c03ed60c9125babd4. --- Cargo.lock | 17 +---- Cargo.toml | 70 +++++++++---------- bin/sozo/Cargo.toml | 1 - crates/dojo-utils/Cargo.toml | 19 ----- crates/dojo-world/Cargo.toml | 5 +- crates/dojo-world/src/lib.rs | 2 + crates/dojo-world/src/migration/mod.rs | 3 +- .../src/lib.rs => dojo-world/src/utils.rs} | 0 crates/sozo/ops/Cargo.toml | 1 - crates/sozo/ops/src/migration/mod.rs | 2 +- crates/sozo/ops/src/utils.rs | 2 +- crates/torii/graphql/Cargo.toml | 7 +- crates/torii/graphql/src/tests/mod.rs | 2 +- 13 files changed, 47 insertions(+), 84 deletions(-) delete mode 100644 crates/dojo-utils/Cargo.toml rename crates/{dojo-utils/src/lib.rs => dojo-world/src/utils.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index b33d983ed4..ef13aa39f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3392,18 +3392,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "dojo-utils" -version = "0.6.0-alpha.7" -dependencies = [ - "assert_matches", - "dojo-test-utils", - "futures", - "starknet 0.9.0", - "thiserror", - "tokio", -] - [[package]] name = "dojo-world" version = "0.6.0-alpha.7" @@ -3421,7 +3409,7 @@ dependencies = [ "dojo-lang", "dojo-test-utils", "dojo-types", - "dojo-utils", + "futures", "http", "ipfs-api-backend-hyper", "scarb", @@ -11158,7 +11146,6 @@ dependencies = [ "dojo-lang", "dojo-test-utils", "dojo-types", - "dojo-utils", "dojo-world", "futures", "katana-runner", @@ -11213,7 +11200,6 @@ dependencies = [ "dojo-lang", "dojo-test-utils", "dojo-types", - "dojo-utils", "dojo-world", "futures", "katana-runner", @@ -12674,7 +12660,6 @@ dependencies = [ "convert_case 0.6.0", "dojo-test-utils", "dojo-types", - "dojo-utils", "dojo-world", "lazy_static", "regex", diff --git a/Cargo.toml b/Cargo.toml index 134d4d39be..991ee50676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,41 +2,40 @@ resolver = "2" members = [ - "bin/dojo-language-server", - "bin/katana", - "bin/saya", - "bin/sozo", - "bin/torii", - "crates/benches", - "crates/common", - "crates/dojo-bindgen", - "crates/dojo-core", - "crates/dojo-lang", - "crates/dojo-test-utils", - "crates/dojo-types", - "crates/dojo-utils", - "crates/dojo-world", - "crates/dojo-world/abigen", - "crates/katana/core", - "crates/katana/executor", - "crates/katana/primitives", - "crates/katana/rpc/rpc", - "crates/katana/rpc/rpc-api", - "crates/katana/rpc/rpc-types", - "crates/katana/rpc/rpc-types-builder", - "crates/katana/runner", - "crates/katana/runner/runner-macro", - "crates/katana/storage/codecs", - "crates/katana/storage/codecs/derive", - "crates/katana/storage/db", - "crates/katana/storage/provider", - "crates/metrics", - "crates/saya/core", - "crates/sozo/signers", - "crates/torii/client", - "crates/torii/server", - "crates/torii/types-test", - "examples/spawn-and-move", + "bin/dojo-language-server", + "bin/katana", + "bin/saya", + "bin/sozo", + "bin/torii", + "crates/benches", + "crates/common", + "crates/dojo-bindgen", + "crates/dojo-core", + "crates/dojo-lang", + "crates/dojo-test-utils", + "crates/dojo-types", + "crates/dojo-world", + "crates/dojo-world/abigen", + "crates/katana/core", + "crates/katana/executor", + "crates/katana/primitives", + "crates/katana/rpc/rpc", + "crates/katana/rpc/rpc-api", + "crates/katana/rpc/rpc-types", + "crates/katana/rpc/rpc-types-builder", + "crates/katana/runner", + "crates/katana/runner/runner-macro", + "crates/katana/storage/codecs", + "crates/katana/storage/codecs/derive", + "crates/katana/storage/db", + "crates/katana/storage/provider", + "crates/metrics", + "crates/saya/core", + "crates/sozo/signers", + "crates/torii/client", + "crates/torii/server", + "crates/torii/types-test", + "examples/spawn-and-move", ] [workspace.package] @@ -64,7 +63,6 @@ dojo-core = { path = "crates/dojo-core" } dojo-lang = { path = "crates/dojo-lang" } dojo-test-utils = { path = "crates/dojo-test-utils" } dojo-types = { path = "crates/dojo-types" } -dojo-utils = { path = "crates/dojo-utils" } dojo-world = { path = "crates/dojo-world" } # katana diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index 429d8731f6..1615ffe48d 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -28,7 +28,6 @@ console.workspace = true dojo-bindgen.workspace = true dojo-lang.workspace = true dojo-types.workspace = true -dojo-utils.workspace = true dojo-world = { workspace = true, features = [ "contracts", "metadata", "migration" ] } futures.workspace = true notify = "6.0.1" diff --git a/crates/dojo-utils/Cargo.toml b/crates/dojo-utils/Cargo.toml deleted file mode 100644 index 8d599ab558..0000000000 --- a/crates/dojo-utils/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -edition.workspace = true -license-file.workspace = true -license.workspace = true -name = "dojo-utils" -repository.workspace = true -version.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -futures.workspace = true -starknet.workspace = true -thiserror.workspace = true -tokio = { version = "1.32.0", features = [ "time" ], default-features = false } - -[dev-dependencies] -assert_matches.workspace = true -dojo-test-utils.workspace = true diff --git a/crates/dojo-world/Cargo.toml b/crates/dojo-world/Cargo.toml index be6e241803..728721454c 100644 --- a/crates/dojo-world/Cargo.toml +++ b/crates/dojo-world/Cargo.toml @@ -7,8 +7,6 @@ repository.workspace = true version.workspace = true [dependencies] -dojo-utils.workspace = true - anyhow.workspace = true async-trait.workspace = true cairo-lang-filesystem.workspace = true @@ -16,6 +14,7 @@ cairo-lang-project.workspace = true cairo-lang-starknet.workspace = true camino.workspace = true convert_case.workspace = true +futures.workspace = true serde.workspace = true serde_json.workspace = true serde_with.workspace = true @@ -46,4 +45,4 @@ tokio.workspace = true contracts = [ "dep:dojo-types", "dep:http" ] manifest = [ "contracts", "dep:dojo-types", "dep:url" ] metadata = [ "dep:ipfs-api-backend-hyper", "dep:scarb", "dep:url" ] -migration = [ ] +migration = [ "dep:tokio" ] diff --git a/crates/dojo-world/src/lib.rs b/crates/dojo-world/src/lib.rs index e9299cd509..2fb2072b76 100644 --- a/crates/dojo-world/src/lib.rs +++ b/crates/dojo-world/src/lib.rs @@ -6,3 +6,5 @@ pub mod manifest; pub mod metadata; #[cfg(feature = "migration")] pub mod migration; +#[cfg(feature = "migration")] +pub mod utils; // TODO: move to somewhere else diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index 0d5fb085c8..bafb4d25dc 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -6,7 +6,6 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use cairo_lang_starknet::casm_contract_class::CasmContractClass; use cairo_lang_starknet::contract_class::ContractClass; -use dojo_utils::{TransactionWaiter, TransactionWaitingError}; use starknet::accounts::{Account, AccountError, Call, ConnectedAccount, SingleOwnerAccount}; use starknet::core::types::contract::{CompiledClass, SierraClass}; use starknet::core::types::{ @@ -21,6 +20,8 @@ use starknet::providers::{Provider, ProviderError}; use starknet::signers::Signer; use thiserror::Error; +use crate::utils::{TransactionWaiter, TransactionWaitingError}; + pub mod class; pub mod contract; pub mod strategy; diff --git a/crates/dojo-utils/src/lib.rs b/crates/dojo-world/src/utils.rs similarity index 100% rename from crates/dojo-utils/src/lib.rs rename to crates/dojo-world/src/utils.rs diff --git a/crates/sozo/ops/Cargo.toml b/crates/sozo/ops/Cargo.toml index 58c67eba87..15bd2435c4 100644 --- a/crates/sozo/ops/Cargo.toml +++ b/crates/sozo/ops/Cargo.toml @@ -30,7 +30,6 @@ console.workspace = true dojo-bindgen.workspace = true dojo-lang.workspace = true dojo-types.workspace = true -dojo-utils.workspace = true dojo-world = { workspace = true, features = [ "contracts", "metadata", "migration" ] } futures.workspace = true notify = "6.0.1" diff --git a/crates/sozo/ops/src/migration/mod.rs b/crates/sozo/ops/src/migration/mod.rs index 8f6b499cfc..781d330dcb 100644 --- a/crates/sozo/ops/src/migration/mod.rs +++ b/crates/sozo/ops/src/migration/mod.rs @@ -3,7 +3,6 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; use camino::Utf8PathBuf; use dojo_lang::compiler::{ABIS_DIR, BASE_DIR, DEPLOYMENTS_DIR, MANIFESTS_DIR, OVERLAYS_DIR}; -use dojo_utils::TransactionWaiter; use dojo_world::contracts::abi::world::ResourceMetadata; use dojo_world::contracts::cairo_utils; use dojo_world::contracts::world::WorldContract; @@ -18,6 +17,7 @@ use dojo_world::migration::world::WorldDiff; use dojo_world::migration::{ Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, TxConfig, }; +use dojo_world::utils::TransactionWaiter; use scarb::core::Workspace; use scarb_ui::Ui; use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; diff --git a/crates/sozo/ops/src/utils.rs b/crates/sozo/ops/src/utils.rs index 65d3391c49..a2aaf99f96 100644 --- a/crates/sozo/ops/src/utils.rs +++ b/crates/sozo/ops/src/utils.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use dojo_utils::{execution_status_from_maybe_pending_receipt, TransactionWaiter}; +use dojo_world::utils::{execution_status_from_maybe_pending_receipt, TransactionWaiter}; use starknet::core::types::{ExecutionResult, InvokeTransactionResult}; use starknet::providers::Provider; diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 4598f92288..d984c87047 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -17,8 +17,7 @@ async-trait.workspace = true base64.workspace = true chrono.workspace = true convert_case = "0.6.0" -dojo-types.workspace = true -dojo-utils.workspace = true +dojo-types = { path = "../../dojo-types" } lazy_static.workspace = true scarb-ui.workspace = true serde.workspace = true @@ -40,8 +39,8 @@ sozo-ops.workspace = true [dev-dependencies] camino.workspace = true -dojo-test-utils = { workspace = true, features = [ "build-examples" ] } -dojo-world.workspace = true +dojo-test-utils = { path = "../../dojo-test-utils", features = [ "build-examples" ] } +dojo-world = { path = "../../dojo-world" } scarb.workspace = true serial_test = "2.0.0" sozo = { path = "../../../bin/sozo" } diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index a48e2e0228..a96c72d240 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -9,9 +9,9 @@ use dojo_test_utils::sequencer::{ }; use dojo_types::primitive::Primitive; use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; -use dojo_utils::TransactionWaiter; use dojo_world::contracts::WorldContractReader; use dojo_world::manifest::DeploymentManifest; +use dojo_world::utils::TransactionWaiter; use scarb::ops; use serde::Deserialize; use serde_json::Value; From 550b6581fd4118d7e506e492e7927b318ce31077 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Thu, 21 Mar 2024 03:41:47 +0100 Subject: [PATCH 09/18] Use TransactionWaiter --- crates/dojo-world/src/utils.rs | 2 +- crates/sozo/ops/src/account.rs | 46 ++++++++-------------------------- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/crates/dojo-world/src/utils.rs b/crates/dojo-world/src/utils.rs index 83d3101daf..e430402580 100644 --- a/crates/dojo-world/src/utils.rs +++ b/crates/dojo-world/src/utils.rs @@ -48,7 +48,7 @@ pub enum TransactionWaitingError { /// let provider = JsonRpcClient::new(HttpTransport::new(Url::parse("http://localhost:5000").unwrap())); /// /// let tx_hash = FieldElement::from(0xbadbeefu64); -/// let receipt = TransactionWaiter::new(tx_hash, &provider).with_finality(TransactionFinalityStatus::ACCEPTED_ON_L2).await.unwrap(); +/// let receipt = TransactionWaiter::new(tx_hash, &provider).with_tx_status(TransactionFinalityStatus::AcceptedOnL2).await.unwrap(); /// ``` #[must_use = "TransactionWaiter does nothing unless polled"] pub struct TransactionWaiter<'a, P: Provider> { diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index 18db493546..7454832f05 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -1,15 +1,15 @@ use std::io::Write; use std::path::PathBuf; -use std::time::Duration; use anyhow::Result; use colored::Colorize; use colored_json::{ColorMode, Output}; +use dojo_world::utils::TransactionWaiter; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use starknet::accounts::{AccountFactory, AccountFactoryError, OpenZeppelinAccountFactory}; use starknet::core::serde::unsigned_field_element::UfeHex; -use starknet::core::types::{BlockId, BlockTag, ExecutionResult, StarknetError}; +use starknet::core::types::{BlockId, BlockTag, StarknetError, TransactionFinalityStatus}; use starknet::core::utils::get_contract_address; use starknet::macros::felt; use starknet::providers::jsonrpc::HttpTransport; @@ -302,7 +302,14 @@ pub async fn deploy( format!("{:#064x}", account_deployment_tx).bright_yellow(), "sozo account fetch".bright_yellow(), ); - watch_tx(&provider, account_deployment_tx, Duration::from_millis(poll_interval)).await?; + TransactionWaiter::new(account_deployment_tx, &provider) + .with_tx_status(TransactionFinalityStatus::AcceptedOnL2) + .with_interval(poll_interval) + .await?; + eprintln!( + "Transaction {} confirmed", + format!("{:#064x}", account_deployment_tx).bright_yellow() + ); account.deployment = DeploymentStatus::Deployed(DeployedStatus { class_hash: undeployed_status.class_hash, @@ -352,39 +359,6 @@ fn map_starknet_error(err: StarknetError) -> anyhow::Error { } } -pub async fn watch_tx

( - provider: P, - transaction_hash: FieldElement, - poll_interval: Duration, -) -> Result<()> -where - P: Provider, -{ - loop { - match provider.get_transaction_receipt(transaction_hash).await { - Ok(receipt) => match receipt.execution_result() { - ExecutionResult::Succeeded => { - eprintln!( - "Transaction {} confirmed", - format!("{:#064x}", transaction_hash).bright_yellow() - ); - - return Ok(()); - } - ExecutionResult::Reverted { reason } => { - return Err(anyhow::anyhow!("transaction reverted: {}", reason)); - } - }, - Err(ProviderError::StarknetError(StarknetError::TransactionHashNotFound)) => { - eprintln!("Transaction not confirmed yet..."); - } - Err(err) => return Err(err.into()), - } - - tokio::time::sleep(poll_interval).await; - } -} - #[derive(Debug)] pub enum FeeSetting { Manual(FieldElement), From 5b8c1fc3f242193c6b3df117a96363974ab80893 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Fri, 22 Mar 2024 23:59:57 +0100 Subject: [PATCH 10/18] Remove unwraps --- bin/sozo/src/commands/account.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs index 906af2eb8f..427883a3cc 100644 --- a/bin/sozo/src/commands/account.rs +++ b/bin/sozo/src/commands/account.rs @@ -70,7 +70,7 @@ impl AccountArgs { config.tokio_handle().block_on(async { match self.command { AccountCommand::New { signer, force, output } => { - let signer: LocalWallet = signer.signer(env_metadata.as_ref()).unwrap(); + let signer: LocalWallet = signer.signer(env_metadata.as_ref())?; account::new(signer, force, output).await } AccountCommand::Deploy { @@ -82,8 +82,8 @@ impl AccountArgs { poll_interval, file, } => { - let provider = starknet.provider(env_metadata.as_ref()).unwrap(); - let signer = signer.signer(env_metadata.as_ref()).unwrap(); + let provider = starknet.provider(env_metadata.as_ref())?; + let signer = signer.signer(env_metadata.as_ref())?; let fee_setting = fee.into_setting()?; account::deploy( provider, From 60f62e184f65381eecf8591bf1518a51f35048c3 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Sat, 23 Mar 2024 00:22:53 +0100 Subject: [PATCH 11/18] Add sozo account fetch --- bin/sozo/src/commands/account.rs | 19 +++++++++++++ crates/sozo/ops/src/account.rs | 47 ++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs index 427883a3cc..8ce9ef40c6 100644 --- a/bin/sozo/src/commands/account.rs +++ b/bin/sozo/src/commands/account.rs @@ -61,6 +61,21 @@ pub enum AccountCommand { #[clap(help = "Path to the account config file")] file: PathBuf, }, + + #[clap(about = "Fetch account config from an already deployed account contract.")] + Fetch { + #[clap(flatten)] + starknet: StarknetOptions, + + #[clap(long, help = "Overwrite the file if it already exists")] + force: bool, + + #[clap(long, help = "Path to save the account config file")] + output: PathBuf, + + #[clap(help = "Contract address")] + address: FieldElement, + }, } impl AccountArgs { @@ -96,6 +111,10 @@ impl AccountArgs { ) .await } + AccountCommand::Fetch { starknet, force, output, address } => { + let provider = starknet.provider(env_metadata.as_ref())?; + account::fetch(provider, force, output, address).await + } } }) } diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index 7454832f05..ecaf3038e4 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -9,9 +9,11 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use starknet::accounts::{AccountFactory, AccountFactoryError, OpenZeppelinAccountFactory}; use starknet::core::serde::unsigned_field_element::UfeHex; -use starknet::core::types::{BlockId, BlockTag, StarknetError, TransactionFinalityStatus}; +use starknet::core::types::{ + BlockId, BlockTag, FunctionCall, StarknetError, TransactionFinalityStatus, +}; use starknet::core::utils::get_contract_address; -use starknet::macros::felt; +use starknet::macros::{felt, selector}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider, ProviderError}; use starknet::signers::{LocalWallet, Signer, SigningKey}; @@ -333,6 +335,47 @@ pub async fn deploy( Ok(()) } +pub async fn fetch( + provider: JsonRpcClient, + force: bool, + output: PathBuf, + address: FieldElement, +) -> Result<()> { + if output.exists() && !force { + anyhow::bail!("account config file already exists"); + } + + let class_hash = provider.get_class_hash_at(BlockId::Tag(BlockTag::Pending), address).await?; + + let public_key = provider + .call( + FunctionCall { + contract_address: address, + entry_point_selector: selector!("get_public_key"), + calldata: vec![], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await?[0]; + + let variant = + AccountVariant::OpenZeppelin(OzAccountConfig { version: 1, public_key, legacy: false }); + + let account = AccountConfig { + version: 1, + variant, + deployment: DeploymentStatus::Deployed(DeployedStatus { class_hash, address }), + }; + + let mut file = std::fs::File::create(&output)?; + serde_json::to_writer_pretty(&mut file, &account)?; + file.write_all(b"\n")?; + + eprintln!("Downloaded new account config file: {}", std::fs::canonicalize(&output)?.display()); + + Ok(()) +} + fn true_as_default() -> bool { true } From 2a22580915693aa97d7cc9d9166ed5b7f1f87380 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Sat, 23 Mar 2024 01:42:12 +0100 Subject: [PATCH 12/18] Add keystore tests --- bin/sozo/src/commands/keystore.rs | 8 +- .../tests/test_data/keystore/keystore.json | 1 + bin/sozo/tests/test_keystore.rs | 129 ++++++++++++++++++ crates/sozo/ops/src/account.rs | 26 ++-- crates/sozo/ops/src/keystore.rs | 30 +--- 5 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 bin/sozo/tests/test_data/keystore/keystore.json create mode 100644 bin/sozo/tests/test_keystore.rs diff --git a/bin/sozo/src/commands/keystore.rs b/bin/sozo/src/commands/keystore.rs index fab0eb4bf6..f74f102329 100644 --- a/bin/sozo/src/commands/keystore.rs +++ b/bin/sozo/src/commands/keystore.rs @@ -29,8 +29,8 @@ pub enum KeystoreCommand { #[clap(long, help = "Overwrite the file if it already exists")] force: bool, - #[clap(long, help = "Take the private key from stdin instead of prompt")] - private_key_stdin: bool, + #[clap(long, help = "Supply private key from command line option instead of prompt")] + private_key: Option, #[clap(long, help = "Supply password from command line option instead of prompt")] password: Option, @@ -68,8 +68,8 @@ impl KeystoreArgs { pub fn run(self) -> Result<()> { match self.command { KeystoreCommand::New { password, force, file } => keystore::new(password, force, file), - KeystoreCommand::FromKey { force, private_key_stdin, password, file } => { - keystore::from_key(force, private_key_stdin, password, file) + KeystoreCommand::FromKey { force, private_key, password, file } => { + keystore::from_key(force, private_key, password, file) } KeystoreCommand::Inspect { password, raw, file } => { keystore::inspect(password, raw, file) diff --git a/bin/sozo/tests/test_data/keystore/keystore.json b/bin/sozo/tests/test_data/keystore/keystore.json new file mode 100644 index 0000000000..41585d606e --- /dev/null +++ b/bin/sozo/tests/test_data/keystore/keystore.json @@ -0,0 +1 @@ +{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"58141a1f640b439118e841655847b745"},"ciphertext":"7a077a8dcfc4ec1182f8f08768a2efcc31a9740658d9e3560196748fcf8b27a2","kdf":"scrypt","kdfparams":{"dklen":32,"n":8192,"p":1,"r":8,"salt":"a0fff244c4dab8f989cbe2b8d261401847f6c49c2470ca8b5d88296ae0ddce7e"},"mac":"26e659a2acadab44a689b776c07da1bdd435253be884b4deac95ca510ae7d27e"},"id":"c062226f-cfdd-416a-9e5e-a5cf8a8b5d9a","version":3} \ No newline at end of file diff --git a/bin/sozo/tests/test_keystore.rs b/bin/sozo/tests/test_keystore.rs new file mode 100644 index 0000000000..3da687d9d4 --- /dev/null +++ b/bin/sozo/tests/test_keystore.rs @@ -0,0 +1,129 @@ +mod utils; + +use std::fs; + +use assert_fs::fixture::{FileTouch, PathChild}; +use utils::snapbox::get_snapbox; + +#[test] +fn test_keystore_new() { + let pt = assert_fs::TempDir::new().unwrap(); + + get_snapbox() + .arg("keystore") + .arg("new") + .arg("keystore.json") + .arg("--password") + .arg("password") + .current_dir(&pt) + .assert() + .success(); + + assert!(pt.child("keystore.json").exists()); +} + +#[test] +fn test_keystore_new_force() { + let pt = assert_fs::TempDir::new().unwrap(); + + pt.child("keystore.json").touch().unwrap(); + + get_snapbox() + .arg("keystore") + .arg("new") + .arg("--password") + .arg("password") + .arg("keystore.json") + .arg("--force") + .current_dir(&pt) + .assert() + .success(); + + assert!(pt.child("keystore.json").exists()); + + let contents = fs::read_to_string(pt.child("keystore.json")).unwrap(); + assert!(!contents.is_empty()); +} + +#[test] +fn test_keystore_from_key() { + let pt = assert_fs::TempDir::new().unwrap(); + + get_snapbox() + .arg("keystore") + .arg("from-key") + .arg("keystore.json") + .arg("--password") + .arg("password") + .arg("--private-key") + .arg("0x123") + .current_dir(&pt) + .assert() + .success(); + + assert!(pt.child("keystore.json").exists()); +} + +#[test] +fn test_keystore_inspect() { + let path = fs::canonicalize("./tests/test_data/keystore").unwrap(); + + let assert = get_snapbox() + .arg("keystore") + .arg("inspect") + .arg("keystore.json") + .arg("--password") + .arg("password") + .current_dir(path) + .assert() + .success(); + + let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + + assert_eq!( + output.trim(), + "Public key: 0x0566d69d8c99f62bc71118399bab25c1f03719463eab8d6a444cd11ece131616" + ) +} + +#[test] +fn test_keystore_inspect_raw() { + let path = fs::canonicalize("./tests/test_data/keystore").unwrap(); + + let assert = get_snapbox() + .arg("keystore") + .arg("inspect") + .arg("keystore.json") + .arg("--password") + .arg("password") + .arg("--raw") + .current_dir(path) + .assert() + .success(); + + let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + + assert_eq!(output.trim(), "0x0566d69d8c99f62bc71118399bab25c1f03719463eab8d6a444cd11ece131616") +} + +#[test] +fn test_keystore_inspect_private() { + let path = fs::canonicalize("./tests/test_data/keystore").unwrap(); + + let assert = get_snapbox() + .arg("keystore") + .arg("inspect-private") + .arg("keystore.json") + .arg("--password") + .arg("password") + .current_dir(path) + .assert() + .success(); + + let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + + assert_eq!( + output.trim(), + "Private key: 0x0000000000000000000000000000000000000000000000000000000000000123" + ) +} diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index ecaf3038e4..69bea608dc 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -113,6 +113,19 @@ impl MaxFeeType { } } +#[derive(Debug)] +pub enum FeeSetting { + Manual(FieldElement), + EstimateOnly, + None, +} + +impl FeeSetting { + pub fn is_estimate_only(&self) -> bool { + matches!(self, FeeSetting::EstimateOnly) + } +} + pub async fn new(signer: LocalWallet, force: bool, output: PathBuf) -> Result<()> { if output.exists() && !force { anyhow::bail!("account config file already exists"); @@ -401,16 +414,3 @@ fn map_starknet_error(err: StarknetError) -> anyhow::Error { err => anyhow::anyhow!("{}", err), } } - -#[derive(Debug)] -pub enum FeeSetting { - Manual(FieldElement), - EstimateOnly, - None, -} - -impl FeeSetting { - pub fn is_estimate_only(&self) -> bool { - matches!(self, FeeSetting::EstimateOnly) - } -} diff --git a/crates/sozo/ops/src/keystore.rs b/crates/sozo/ops/src/keystore.rs index 6ab29f52fa..a8d853ca80 100644 --- a/crates/sozo/ops/src/keystore.rs +++ b/crates/sozo/ops/src/keystore.rs @@ -1,4 +1,3 @@ -use std::io::Read; use std::path::PathBuf; use anyhow::Result; @@ -6,15 +5,7 @@ use colored::Colorize; use starknet::signers::SigningKey; use starknet_crypto::FieldElement; -const RAW_PASSWORD_WARNING: &str = "WARNING: setting passwords via --password is generally \ - considered insecure, as they will be stored in your shell \ - history or other log files."; - pub fn new(password: Option, force: bool, file: PathBuf) -> Result<()> { - if password.is_some() { - eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); - } - if file.exists() && !force { anyhow::bail!("keystore file already exists"); } @@ -32,23 +23,16 @@ pub fn new(password: Option, force: bool, file: PathBuf) -> Result<()> { pub fn from_key( force: bool, - private_key_stdin: bool, + private_key: Option, password: Option, file: PathBuf, ) -> Result<()> { - if password.is_some() { - eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); - } - if file.exists() && !force { anyhow::bail!("keystore file already exists"); } - let private_key = if private_key_stdin { - let mut buffer = String::new(); - std::io::stdin().read_to_string(&mut buffer)?; - - buffer + let private_key = if let Some(private_key) = private_key { + private_key } else { rpassword::prompt_password("Enter private key: ")? }; @@ -66,10 +50,6 @@ pub fn from_key( } pub fn inspect(password: Option, raw: bool, file: PathBuf) -> Result<()> { - if password.is_some() { - eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); - } - if !file.exists() { anyhow::bail!("keystore file not found"); } @@ -88,10 +68,6 @@ pub fn inspect(password: Option, raw: bool, file: PathBuf) -> Result<()> } pub fn inspect_private(password: Option, raw: bool, file: PathBuf) -> Result<()> { - if password.is_some() { - eprintln!("{}", RAW_PASSWORD_WARNING.bright_magenta()); - } - if !file.exists() { anyhow::bail!("keystore file not found"); } From 3aeb70b1a56de8a56b66f835e74a5f55880cb6e2 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Sat, 23 Mar 2024 03:31:31 +0100 Subject: [PATCH 13/18] Add account tests --- bin/sozo/tests/test_account.rs | 84 +++++++++++++++++++ bin/sozo/tests/test_data/account/account.json | 14 ++++ 2 files changed, 98 insertions(+) create mode 100644 bin/sozo/tests/test_account.rs create mode 100644 bin/sozo/tests/test_data/account/account.json diff --git a/bin/sozo/tests/test_account.rs b/bin/sozo/tests/test_account.rs new file mode 100644 index 0000000000..e502e89036 --- /dev/null +++ b/bin/sozo/tests/test_account.rs @@ -0,0 +1,84 @@ +mod utils; + +use std::{env, fs}; + +use assert_fs::fixture::PathChild; +use starknet_crypto::FieldElement; +use utils::snapbox::get_snapbox; + +use sozo_ops::account::{self, FeeSetting}; + +use starknet::{ + accounts::Account, + signers::{LocalWallet, SigningKey}, +}; + +#[test] +fn test_account_new() { + let pt = assert_fs::TempDir::new().unwrap(); + let dst_path = pt.child("keystore.json"); + let src_path = fs::canonicalize("./tests/test_data/keystore/keystore.json").unwrap(); + fs::copy(src_path, dst_path).unwrap(); + + get_snapbox() + .arg("account") + .arg("new") + .arg("account.json") + .arg("--keystore") + .arg("keystore.json") + .arg("--password") + .arg("password") + .current_dir(&pt) + .assert() + .success(); + + assert!(pt.child("account.json").exists()); +} + +#[katana_runner::katana_test(1, true)] +async fn test_account_deploy() { + let account_path = fs::canonicalize("./tests/test_data/account/account.json").unwrap(); + let keystore_path = fs::canonicalize("./tests/test_data/keystore/keystore.json").unwrap(); + + let pt = assert_fs::TempDir::new().unwrap(); + fs::copy(account_path.clone(), pt.child("account.json")).unwrap(); + + let signer = LocalWallet::from_signing_key( + SigningKey::from_keystore(keystore_path, "password").unwrap(), + ); + + let contents = fs::read_to_string(pt.child("account.json")).unwrap(); + assert!(contents.contains(r#""status": "undeployed""#)); + + env::set_var("PASS_STDIN", "1"); + account::deploy( + runner.owned_provider(), + signer, + FeeSetting::Manual(FieldElement::from(1000_u32)), + false, + None, + 1000, + account_path, + ) + .await + .unwrap(); + + let contents = fs::read_to_string(pt.child("account.json")).unwrap(); + assert!(contents.contains(r#""status": "deployed""#)); +} + +#[katana_runner::katana_test(1, true)] +async fn test_account_fetch() { + let pt = assert_fs::TempDir::new().unwrap(); + + account::fetch( + runner.owned_provider(), + false, + pt.child("account.json").to_path_buf(), + runner.account(1).address(), + ) + .await + .unwrap(); + + assert!(pt.child("account.json").exists()); +} diff --git a/bin/sozo/tests/test_data/account/account.json b/bin/sozo/tests/test_data/account/account.json new file mode 100644 index 0000000000..542b9b9c4c --- /dev/null +++ b/bin/sozo/tests/test_data/account/account.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "variant": { + "type": "open_zeppelin", + "version": 1, + "public_key": "0x566d69d8c99f62bc71118399bab25c1f03719463eab8d6a444cd11ece131616", + "legacy": false + }, + "deployment": { + "status": "undeployed", + "class_hash": "0x5400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c", + "salt": "0x39ee9015d35fae61ebf7348b4630b849033bd223fdd96f1e8c8d54e4611ebf2" + } +} From 692869b05b8891d8437c2b8283de7b0a968767e1 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Sat, 23 Mar 2024 03:34:50 +0100 Subject: [PATCH 14/18] Remove test_account_deploy --- bin/sozo/tests/test_account.rs | 44 +++------------------------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/bin/sozo/tests/test_account.rs b/bin/sozo/tests/test_account.rs index e502e89036..0d42870286 100644 --- a/bin/sozo/tests/test_account.rs +++ b/bin/sozo/tests/test_account.rs @@ -1,18 +1,12 @@ mod utils; -use std::{env, fs}; +use std::fs; use assert_fs::fixture::PathChild; -use starknet_crypto::FieldElement; +use sozo_ops::account; +use starknet::accounts::Account; use utils::snapbox::get_snapbox; -use sozo_ops::account::{self, FeeSetting}; - -use starknet::{ - accounts::Account, - signers::{LocalWallet, SigningKey}, -}; - #[test] fn test_account_new() { let pt = assert_fs::TempDir::new().unwrap(); @@ -35,38 +29,6 @@ fn test_account_new() { assert!(pt.child("account.json").exists()); } -#[katana_runner::katana_test(1, true)] -async fn test_account_deploy() { - let account_path = fs::canonicalize("./tests/test_data/account/account.json").unwrap(); - let keystore_path = fs::canonicalize("./tests/test_data/keystore/keystore.json").unwrap(); - - let pt = assert_fs::TempDir::new().unwrap(); - fs::copy(account_path.clone(), pt.child("account.json")).unwrap(); - - let signer = LocalWallet::from_signing_key( - SigningKey::from_keystore(keystore_path, "password").unwrap(), - ); - - let contents = fs::read_to_string(pt.child("account.json")).unwrap(); - assert!(contents.contains(r#""status": "undeployed""#)); - - env::set_var("PASS_STDIN", "1"); - account::deploy( - runner.owned_provider(), - signer, - FeeSetting::Manual(FieldElement::from(1000_u32)), - false, - None, - 1000, - account_path, - ) - .await - .unwrap(); - - let contents = fs::read_to_string(pt.child("account.json")).unwrap(); - assert!(contents.contains(r#""status": "deployed""#)); -} - #[katana_runner::katana_test(1, true)] async fn test_account_fetch() { let pt = assert_fs::TempDir::new().unwrap(); From a6d963c572b0b4417bc5ac99ca20ed776479d014 Mon Sep 17 00:00:00 2001 From: JimmyFate Date: Sat, 30 Mar 2024 01:19:11 +0100 Subject: [PATCH 15/18] output -> file --- Cargo.lock | 2 +- bin/sozo/src/commands/account.rs | 6 +++--- crates/sozo/ops/src/account.rs | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39746fa66a..05774a8acc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,7 +1002,7 @@ dependencies = [ "libm", "num-bigint", "num-integer", - "num-traits 0.2.17", + "num-traits 0.2.18", ] [[package]] diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs index 8ce9ef40c6..9e97a87aa5 100644 --- a/bin/sozo/src/commands/account.rs +++ b/bin/sozo/src/commands/account.rs @@ -30,7 +30,7 @@ pub enum AccountCommand { force: bool, #[clap(help = "Path to save the account config file")] - output: PathBuf, + file: PathBuf, }, #[clap(about = "Deploy account contract with a DeployAccount transaction.")] @@ -84,9 +84,9 @@ impl AccountArgs { config.tokio_handle().block_on(async { match self.command { - AccountCommand::New { signer, force, output } => { + AccountCommand::New { signer, force, file } => { let signer: LocalWallet = signer.signer(env_metadata.as_ref())?; - account::new(signer, force, output).await + account::new(signer, force, file).await } AccountCommand::Deploy { starknet, diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index 69bea608dc..040af0338c 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -126,8 +126,8 @@ impl FeeSetting { } } -pub async fn new(signer: LocalWallet, force: bool, output: PathBuf) -> Result<()> { - if output.exists() && !force { +pub async fn new(signer: LocalWallet, force: bool, file: PathBuf) -> Result<()> { + if file.exists() && !force { anyhow::bail!("account config file already exists"); } @@ -148,11 +148,12 @@ pub async fn new(signer: LocalWallet, force: bool, output: PathBuf) -> Result<() let deployed_address = account_config.deploy_account_address()?; - let mut file = std::fs::File::create(&output)?; + let file_path = file; + let mut file = std::fs::File::create(&file_path)?; serde_json::to_writer_pretty(&mut file, &account_config)?; file.write_all(b"\n")?; - eprintln!("Created new account config file: {}", std::fs::canonicalize(&output)?.display()); + eprintln!("Created new account config file: {}", std::fs::canonicalize(&file_path)?.display()); eprintln!(); eprintln!( "Once deployed, this account will be available at:\n {}", @@ -161,7 +162,7 @@ pub async fn new(signer: LocalWallet, force: bool, output: PathBuf) -> Result<() eprintln!(); eprintln!( "Deploy this account by running:\n {}", - format!("sozo account deploy {}", output.display()).bright_yellow() + format!("sozo account deploy {}", file_path.display()).bright_yellow() ); Ok(()) From 25c729a5ea38bdbd74a702203cc6d5186cb32f7f Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Thu, 11 Apr 2024 15:08:36 +0530 Subject: [PATCH 16/18] add --no-confirmation flag and refactor deploy function --- bin/sozo/src/commands/account.rs | 5 ++ crates/sozo/ops/src/account.rs | 150 +++++++++++++++++++++---------- 2 files changed, 106 insertions(+), 49 deletions(-) diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs index 9e97a87aa5..f22e11f511 100644 --- a/bin/sozo/src/commands/account.rs +++ b/bin/sozo/src/commands/account.rs @@ -60,6 +60,9 @@ pub enum AccountCommand { #[clap(help = "Path to the account config file")] file: PathBuf, + + #[clap(long, help = "Don't wait for user confirmation")] + no_confirmation: bool, }, #[clap(about = "Fetch account config from an already deployed account contract.")] @@ -96,6 +99,7 @@ impl AccountArgs { nonce, poll_interval, file, + no_confirmation, } => { let provider = starknet.provider(env_metadata.as_ref())?; let signer = signer.signer(env_metadata.as_ref())?; @@ -108,6 +112,7 @@ impl AccountArgs { nonce, poll_interval, file, + no_confirmation, ) .await } diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index 040af0338c..b2c535199f 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -1,3 +1,4 @@ +use core::panic; use std::io::Write; use std::path::PathBuf; @@ -81,6 +82,22 @@ pub enum DeploymentStatus { Deployed(DeployedStatus), } +impl DeploymentStatus { + pub fn to_deployed(&mut self, address: FieldElement) { + match self { + DeploymentStatus::Undeployed(status) => { + *self = DeploymentStatus::Deployed(DeployedStatus { + class_hash: status.class_hash, + address, + }); + } + DeploymentStatus::Deployed(_) => { + panic!("Already deployed!") + } + } + } +} + #[serde_as] #[derive(Serialize, Deserialize)] pub struct UndeployedStatus { @@ -176,6 +193,7 @@ pub async fn deploy( nonce: Option, poll_interval: u64, file: PathBuf, + no_confirmation: bool, ) -> Result<()> { if simulate && fee_setting.is_estimate_only() { anyhow::bail!("--simulate cannot be used with --estimate-only"); @@ -187,8 +205,6 @@ pub async fn deploy( let mut account: AccountConfig = serde_json::from_reader(&mut std::fs::File::open(&file)?)?; - let signer_public_key = signer.get_public_key().await?.scalar(); - let undeployed_status = match &account.deployment { DeploymentStatus::Undeployed(inner) => inner, DeploymentStatus::Deployed(_) => { @@ -201,6 +217,7 @@ pub async fn deploy( let factory = match &account.variant { AccountVariant::OpenZeppelin(oz_config) => { // Makes sure we're using the right key + let signer_public_key = signer.get_public_key().await?.scalar(); if signer_public_key != oz_config.public_key { anyhow::bail!( "public key mismatch. Expected: {:#064x}; actual: {:#064x}.", @@ -259,35 +276,6 @@ pub async fn deploy( } }; - if !simulate { - match max_fee { - MaxFeeType::Manual { max_fee } => { - eprintln!( - "You've manually specified the account deployment fee to be {}. Therefore, \ - fund at least:\n {}", - format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), - format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), - ); - } - MaxFeeType::Estimated { estimate, estimate_with_buffer } => { - eprintln!( - "The estimated account deployment fee is {}. However, to avoid failure, fund \ - at least:\n {}", - format!("{} ETH", estimate.to_big_decimal(18)).bright_yellow(), - format!("{} ETH", estimate_with_buffer.to_big_decimal(18)).bright_yellow() - ); - } - } - - eprintln!( - "to the following address:\n {}", - format!("{:#064x}", target_deployment_address).bright_yellow() - ); - - eprint!("Press [ENTER] once you've funded the address."); - std::io::stdin().read_line(&mut String::new())?; - } - let account_deployment = match nonce { Some(nonce) => account_deployment.nonce(nonce), None => account_deployment, @@ -295,26 +283,72 @@ pub async fn deploy( let account_deployment = account_deployment.max_fee(max_fee.max_fee()); if simulate { - let simulation = account_deployment.simulate(false, false).await?; - let simulation_json = serde_json::to_value(simulation)?; - - let simulation_json = - colored_json::to_colored_json(&simulation_json, ColorMode::Auto(Output::StdOut))?; - println!("{simulation_json}"); + simulate_account_deploy(&account_deployment).await?; return Ok(()); + } else { + do_accoun_deploy( + max_fee, + target_deployment_address, + no_confirmation, + account_deployment, + &provider, + poll_interval, + &mut account, + ) + .await?; + + write_account_to_file(file, account)?; + + Ok(()) } +} +async fn do_account_deploy( + max_fee: MaxFeeType, + target_deployment_address: FieldElement, + no_confirmation: bool, + account_deployment: starknet::accounts::AccountDeployment< + '_, + OpenZeppelinAccountFactory>, + >, + provider: &JsonRpcClient, + poll_interval: u64, + account: &mut AccountConfig, +) -> Result<(), anyhow::Error> { + match max_fee { + MaxFeeType::Manual { max_fee } => { + eprintln!( + "You've manually specified the account deployment fee to be {}. Therefore, \ + fund at least:\n {}", + format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), + format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), + ); + } + MaxFeeType::Estimated { estimate, estimate_with_buffer } => { + eprintln!( + "The estimated account deployment fee is {}. However, to avoid failure, fund \ + at least:\n {}", + format!("{} ETH", estimate.to_big_decimal(18)).bright_yellow(), + format!("{} ETH", estimate_with_buffer.to_big_decimal(18)).bright_yellow() + ); + } + } + eprintln!( + "to the following address:\n {}", + format!("{:#064x}", target_deployment_address).bright_yellow() + ); + if !no_confirmation { + eprint!("Press [ENTER] once you've funded the address."); + std::io::stdin().read_line(&mut String::new())?; + } let account_deployment_tx = account_deployment.send().await?.transaction_hash; eprintln!( "Account deployment transaction: {}", format!("{:#064x}", account_deployment_tx).bright_yellow() ); - - // By default we wait for the tx to confirm so that we don't incorrectly mark the - // account as deployed eprintln!( - "Waiting for transaction {} to confirm. If this process is interrupted, you will need to \ - run `{}` to update the account file.", + "Waiting for transaction {} to confirm. If this process is interrupted, you will need \ + to run `{}` to update the account file.", format!("{:#064x}", account_deployment_tx).bright_yellow(), "sozo account fetch".bright_yellow(), ); @@ -327,28 +361,46 @@ pub async fn deploy( format!("{:#064x}", account_deployment_tx).bright_yellow() ); - account.deployment = DeploymentStatus::Deployed(DeployedStatus { - class_hash: undeployed_status.class_hash, - address: target_deployment_address, - }); + account.deployment.to_deployed(target_deployment_address); - // Never write directly to the original file to avoid data loss + Ok(()) +} + +fn write_account_to_file(file: PathBuf, account: AccountConfig) -> Result<(), anyhow::Error> { let mut temp_file_name = file .file_name() .ok_or_else(|| anyhow::anyhow!("unable to determine file name"))? .to_owned(); + + // Never write directly to the original file to avoid data loss temp_file_name.push(".tmp"); + let mut temp_path = file.clone(); temp_path.set_file_name(temp_file_name); let mut temp_file = std::fs::File::create(&temp_path)?; serde_json::to_writer_pretty(&mut temp_file, &account)?; - temp_file.write_all(b"\n")?; - std::fs::rename(temp_path, file)?; + // temp_file.write_all(b"\n")?; + std::fs::rename(temp_path, file)?; Ok(()) } +async fn simulate_account_deploy( + account_deployment: &starknet::accounts::AccountDeployment< + '_, + OpenZeppelinAccountFactory>, + >, +) -> Result<(), anyhow::Error> { + let simulation = account_deployment.simulate(false, false).await?; + let simulation_json = serde_json::to_value(simulation)?; + let simulation_json = + colored_json::to_colored_json(&simulation_json, ColorMode::Auto(Output::StdOut))?; + + println!("{simulation_json}"); + return Ok(()); +} + pub async fn fetch( provider: JsonRpcClient, force: bool, From f10c7ee6d99bbb243bd77c5928ef4ea2e95d0d7c Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Thu, 11 Apr 2024 15:25:57 +0530 Subject: [PATCH 17/18] fix typo --- crates/sozo/ops/src/account.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sozo/ops/src/account.rs b/crates/sozo/ops/src/account.rs index 149c14be60..4460227f35 100644 --- a/crates/sozo/ops/src/account.rs +++ b/crates/sozo/ops/src/account.rs @@ -286,7 +286,7 @@ pub async fn deploy( simulate_account_deploy(&account_deployment).await?; return Ok(()); } else { - do_accoun_deploy( + do_account_deploy( max_fee, target_deployment_address, no_confirmation, From 64271ff3275c8764ca9521d85c464a5570e8011e Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Sun, 21 Apr 2024 01:27:14 +0530 Subject: [PATCH 18/18] add `no_wait` for test --- bin/sozo/src/commands/account.rs | 4 ++-- bin/sozo/src/commands/options/account.rs | 2 +- bin/sozo/src/commands/options/signer.rs | 24 +++++++++++++++--------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/bin/sozo/src/commands/account.rs b/bin/sozo/src/commands/account.rs index f22e11f511..fd99c9756b 100644 --- a/bin/sozo/src/commands/account.rs +++ b/bin/sozo/src/commands/account.rs @@ -88,7 +88,7 @@ impl AccountArgs { config.tokio_handle().block_on(async { match self.command { AccountCommand::New { signer, force, file } => { - let signer: LocalWallet = signer.signer(env_metadata.as_ref())?; + let signer: LocalWallet = signer.signer(env_metadata.as_ref(), false)?; account::new(signer, force, file).await } AccountCommand::Deploy { @@ -102,7 +102,7 @@ impl AccountArgs { no_confirmation, } => { let provider = starknet.provider(env_metadata.as_ref())?; - let signer = signer.signer(env_metadata.as_ref())?; + let signer = signer.signer(env_metadata.as_ref(), false)?; let fee_setting = fee.into_setting()?; account::deploy( provider, diff --git a/bin/sozo/src/commands/options/account.rs b/bin/sozo/src/commands/options/account.rs index 5f93c07aaa..2ad778e747 100644 --- a/bin/sozo/src/commands/options/account.rs +++ b/bin/sozo/src/commands/options/account.rs @@ -41,7 +41,7 @@ impl AccountOptions { P: Provider + Send + Sync, { let account_address = self.account_address(env_metadata)?; - let signer = self.signer.signer(env_metadata)?; + let signer = self.signer.signer(env_metadata, false)?; let chain_id = provider.chain_id().await.with_context(|| "Failed to retrieve network chain id.")?; diff --git a/bin/sozo/src/commands/options/signer.rs b/bin/sozo/src/commands/options/signer.rs index 311674d5d4..9a9a14e128 100644 --- a/bin/sozo/src/commands/options/signer.rs +++ b/bin/sozo/src/commands/options/signer.rs @@ -36,7 +36,7 @@ pub struct SignerOptions { } impl SignerOptions { - pub fn signer(&self, env_metadata: Option<&Environment>) -> Result { + pub fn signer(&self, env_metadata: Option<&Environment>, no_wait: bool) -> Result { if let Some(private_key) = self.private_key.as_deref().or_else(|| env_metadata.and_then(|env| env.private_key())) { @@ -58,7 +58,13 @@ impl SignerOptions { { password.to_owned() } else { - rpassword::prompt_password("Enter password: ")? + if no_wait { + return Err(anyhow!( + "Could not find password. Please specify the password." + )); + } else { + rpassword::prompt_password("Enter password: ")? + } } }; let private_key = SigningKey::from_keystore(path, &password)?; @@ -109,7 +115,7 @@ mod tests { let private_key = "0x1"; let cmd = Command::parse_from(["sozo", "--private-key", private_key]); - let result_wallet = cmd.signer.signer(None).unwrap(); + let result_wallet = cmd.signer.signer(None, true).unwrap(); let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( FieldElement::from_str(private_key).unwrap(), )); @@ -128,7 +134,7 @@ mod tests { }; let cmd = Command::parse_from(["sozo"]); - let result_wallet = cmd.signer.signer(Some(&env_metadata)).unwrap(); + let result_wallet = cmd.signer.signer(Some(&env_metadata), true).unwrap(); let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( FieldElement::from_str(private_key).unwrap(), )); @@ -151,7 +157,7 @@ mod tests { "--password", keystore_password, ]); - let result_wallet = cmd.signer.signer(None).unwrap(); + let result_wallet = cmd.signer.signer(None, true).unwrap(); let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( FieldElement::from_str(private_key).unwrap(), )); @@ -173,7 +179,7 @@ mod tests { }; let cmd = Command::parse_from(["sozo", "--password", keystore_password]); - let result_wallet = cmd.signer.signer(Some(&env_metadata)).unwrap(); + let result_wallet = cmd.signer.signer(Some(&env_metadata), true).unwrap(); let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( FieldElement::from_str(private_key).unwrap(), )); @@ -195,7 +201,7 @@ mod tests { }; let cmd = Command::parse_from(["sozo", "--keystore", keystore_path]); - let result_wallet = cmd.signer.signer(Some(&env_metadata)).unwrap(); + let result_wallet = cmd.signer.signer(Some(&env_metadata), true).unwrap(); let expected_wallet = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( FieldElement::from_str(private_key).unwrap(), )); @@ -224,7 +230,7 @@ mod tests { let keystore_path = "./tests/test_data/keystore/test.json"; let cmd = Command::parse_from(["sozo", "--keystore", keystore_path]); - let result = cmd.signer.signer(None); + let result = cmd.signer.signer(None, true); assert!(result.is_err()); } @@ -232,7 +238,7 @@ mod tests { #[test] fn signer_without_pk_or_keystore() { let cmd = Command::parse_from(["sozo"]); - let result = cmd.signer.signer(None); + let result = cmd.signer.signer(None, true); assert!(result.is_err()); }