diff --git a/CHANGELOG.md b/CHANGELOG.md index d0bdb26b67..da2f7cdad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Short option for `--accounts-file` flag has been removed. - Short option for `--contract-address` is now `-d` instead of `-a`. +- `account add` is renamed to `account import`. +- `account import` can be now used without specifying `--private-key` or `--private-key-file` flags. Instead private key will be read interactively from the user. #### Fixed - `account delete` command: It is no longer necessary to provide the `--url` argument each time. Either the `--url` or `--network` argument must be provided, but not both, as they are mutually exclusive. diff --git a/crates/sncast/src/main.rs b/crates/sncast/src/main.rs index 4a5b91442d..b4a0554605 100644 --- a/crates/sncast/src/main.rs +++ b/crates/sncast/src/main.rs @@ -349,17 +349,17 @@ async fn run_async_command( } Commands::Account(account) => match account.command { - account::Commands::Add(add) => { - let provider = add.rpc.get_provider(&config).await?; - let result = starknet_commands::account::add::add( - &add.name.clone(), + account::Commands::Import(import) => { + let provider = import.rpc.get_provider(&config).await?; + let result = starknet_commands::account::import::import( + &import.name.clone(), &config.accounts_file, &provider, - &add, + &import, ) .await; - print_command_result("account add", &result, numbers_format, output_format)?; + print_command_result("account import", &result, numbers_format, output_format)?; Ok(()) } diff --git a/crates/sncast/src/response/structs.rs b/crates/sncast/src/response/structs.rs index 9da90ae1b3..21245df026 100644 --- a/crates/sncast/src/response/structs.rs +++ b/crates/sncast/src/response/structs.rs @@ -64,11 +64,11 @@ pub struct AccountCreateResponse { impl CommandResponse for AccountCreateResponse {} #[derive(Serialize)] -pub struct AccountAddResponse { +pub struct AccountImportResponse { pub add_profile: String, } -impl CommandResponse for AccountAddResponse {} +impl CommandResponse for AccountImportResponse {} #[derive(Serialize)] pub struct AccountDeleteResponse { diff --git a/crates/sncast/src/starknet_commands/account/add.rs b/crates/sncast/src/starknet_commands/account/add.rs deleted file mode 100644 index 79c09b75d1..0000000000 --- a/crates/sncast/src/starknet_commands/account/add.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::starknet_commands::account::{ - add_created_profile_to_configuration, prepare_account_json, write_account_to_accounts_file, - AccountType, -}; -use anyhow::{ensure, Context, Result}; -use camino::Utf8PathBuf; -use clap::Args; -use sncast::helpers::configuration::CastConfig; -use sncast::helpers::rpc::RpcArgs; -use sncast::response::structs::AccountAddResponse; -use sncast::{check_class_hash_exists, get_chain_id}; -use sncast::{check_if_legacy_contract, get_class_hash_by_address}; -use starknet::core::types::Felt; -use starknet::providers::jsonrpc::{HttpTransport, JsonRpcClient}; -use starknet::signers::SigningKey; - -#[derive(Args, Debug)] -#[command(about = "Add an account to the accounts file")] -pub struct Add { - /// Name of the account to be added - #[clap(short, long)] - pub name: String, - - /// Address of the account - #[clap(short, long, requires = "private_key_input")] - pub address: Felt, - - /// Type of the account - #[clap(short = 't', long = "type")] - pub account_type: AccountType, - - /// Class hash of the account - #[clap(short, long)] - pub class_hash: Option, - - /// Account private key - #[clap(long, group = "private_key_input")] - pub private_key: Option, - - /// Path to the file holding account private key - #[clap(long = "private-key-file", group = "private_key_input")] - pub private_key_file_path: Option, - - /// Account public key - #[clap(long)] - pub public_key: Option, - - /// Salt for the address - #[clap(short, long)] - pub salt: Option, - - /// If passed, a profile with the provided name and corresponding data will be created in snfoundry.toml - #[allow(clippy::struct_field_names)] - #[clap(long)] - pub add_profile: Option, - - #[clap(flatten)] - pub rpc: RpcArgs, -} - -pub async fn add( - account: &str, - accounts_file: &Utf8PathBuf, - provider: &JsonRpcClient, - add: &Add, -) -> Result { - let private_key = match &add.private_key_file_path { - Some(file_path) => get_private_key_from_file(file_path) - .with_context(|| format!("Failed to obtain private key from the file {file_path}"))?, - None => add - .private_key - .expect("Failed to parse provided private key"), - }; - let private_key = &SigningKey::from_secret_scalar(private_key); - if let Some(public_key) = &add.public_key { - ensure!( - public_key == &private_key.verifying_key().scalar(), - "The private key does not match the public key" - ); - } - - let fetched_class_hash = get_class_hash_by_address(provider, add.address).await?; - let deployed = fetched_class_hash.is_some(); - let class_hash = match (fetched_class_hash, add.class_hash) { - (Some(from_provider), Some(from_user)) => { - ensure!( - from_provider == from_user, - "Incorrect class hash {:#x} for account address {:#x}", - from_user, - add.address - ); - fetched_class_hash - } - (None, Some(from_user)) => { - check_class_hash_exists(provider, from_user).await?; - Some(from_user) - } - _ => fetched_class_hash, - }; - - let legacy = check_if_legacy_contract(class_hash, add.address, provider).await?; - - let account_json = prepare_account_json( - private_key, - add.address, - deployed, - legacy, - &add.account_type, - class_hash, - add.salt, - ); - - let chain_id = get_chain_id(provider).await?; - write_account_to_accounts_file(account, accounts_file, chain_id, account_json.clone())?; - - if add.add_profile.is_some() { - let config = CastConfig { - url: add.rpc.url.clone().unwrap_or_default(), - account: account.into(), - accounts_file: accounts_file.into(), - ..Default::default() - }; - add_created_profile_to_configuration(&add.add_profile, &config, &None)?; - } - - Ok(AccountAddResponse { - add_profile: if add.add_profile.is_some() { - format!( - "Profile {} successfully added to snfoundry.toml", - add.add_profile.clone().expect("Failed to get profile name") - ) - } else { - "--add-profile flag was not set. No profile added to snfoundry.toml".to_string() - }, - }) -} - -fn get_private_key_from_file(file_path: &Utf8PathBuf) -> Result { - let private_key_string = std::fs::read_to_string(file_path.clone())?; - Ok(private_key_string.parse()?) -} diff --git a/crates/sncast/src/starknet_commands/account/deploy.rs b/crates/sncast/src/starknet_commands/account/deploy.rs index 0f23deaf6a..094d31cd21 100644 --- a/crates/sncast/src/starknet_commands/account/deploy.rs +++ b/crates/sncast/src/starknet_commands/account/deploy.rs @@ -134,26 +134,7 @@ async fn deploy_from_keystore( .class_hash .context("Failed to get class hash from keystore")?; - let address = match account_type { - AccountType::Argent => get_contract_address( - salt, - class_hash, - &[private_key.verifying_key().scalar(), Felt::ZERO], - Felt::ZERO, - ), - AccountType::OpenZeppelin => get_contract_address( - salt, - class_hash, - &[private_key.verifying_key().scalar()], - chain_id, - ), - AccountType::Braavos => get_contract_address( - salt, - BRAAVOS_BASE_ACCOUNT_CLASS_HASH, - &[private_key.verifying_key().scalar()], - chain_id, - ), - }; + let address = compute_account_address(salt, &private_key, class_hash, account_type, chain_id); let result = if provider .get_class_hash_at(BlockId::Tag(Pending), address) @@ -370,3 +351,32 @@ fn update_keystore_account(account: &str, address: Felt) -> Result<()> { Ok(()) } + +pub(crate) fn compute_account_address( + salt: Felt, + private_key: &SigningKey, + class_hash: Felt, + account_type: AccountType, + chain_id: Felt, +) -> Felt { + match account_type { + AccountType::Argent => get_contract_address( + salt, + class_hash, + &[private_key.verifying_key().scalar(), Felt::ZERO], + Felt::ZERO, + ), + AccountType::OpenZeppelin => get_contract_address( + salt, + class_hash, + &[private_key.verifying_key().scalar()], + chain_id, + ), + AccountType::Braavos => get_contract_address( + salt, + BRAAVOS_BASE_ACCOUNT_CLASS_HASH, + &[private_key.verifying_key().scalar()], + chain_id, + ), + } +} diff --git a/crates/sncast/src/starknet_commands/account/import.rs b/crates/sncast/src/starknet_commands/account/import.rs new file mode 100644 index 0000000000..66d4fd3e11 --- /dev/null +++ b/crates/sncast/src/starknet_commands/account/import.rs @@ -0,0 +1,244 @@ +use crate::starknet_commands::account::{ + add_created_profile_to_configuration, prepare_account_json, write_account_to_accounts_file, + AccountType, +}; +use anyhow::{bail, ensure, Context, Result}; +use camino::Utf8PathBuf; +use clap::Args; +use conversions::string::{TryFromDecStr, TryFromHexStr}; +use regex::Regex; +use sncast::helpers::configuration::CastConfig; +use sncast::helpers::rpc::RpcArgs; +use sncast::response::structs::AccountImportResponse; +use sncast::{check_class_hash_exists, get_chain_id, AccountType as SNCastAccountType}; +use sncast::{check_if_legacy_contract, get_class_hash_by_address}; +use starknet::core::types::Felt; +use starknet::providers::jsonrpc::{HttpTransport, JsonRpcClient}; +use starknet::signers::SigningKey; + +use super::deploy::compute_account_address; + +#[derive(Args, Debug)] +#[command(about = "Add an account to the accounts file")] +pub struct Import { + /// Name of the account to be imported + #[clap(short, long)] + pub name: String, + + /// Address of the account + #[clap(short, long)] + pub address: Felt, + + /// Type of the account + #[clap(short = 't', long = "type")] + pub account_type: AccountType, + + /// Class hash of the account + #[clap(short, long)] + pub class_hash: Option, + + /// Account private key + #[clap(long, group = "private_key_input")] + pub private_key: Option, + + /// Path to the file holding account private key + #[clap(long = "private-key-file", group = "private_key_input")] + pub private_key_file_path: Option, + + /// Salt for the address + #[clap(short, long)] + pub salt: Option, + + /// If passed, a profile with the provided name and corresponding data will be created in snfoundry.toml + #[allow(clippy::struct_field_names)] + #[clap(long)] + pub add_profile: Option, + + #[clap(flatten)] + pub rpc: RpcArgs, +} + +pub async fn import( + account: &str, + accounts_file: &Utf8PathBuf, + provider: &JsonRpcClient, + import: &Import, +) -> Result { + let private_key = if let Some(passed_private_key) = &import.private_key { + passed_private_key + } else if let Some(passed_private_key_file_path) = &import.private_key_file_path { + &get_private_key_from_file(passed_private_key_file_path).with_context(|| { + format!("Failed to obtain private key from the file {passed_private_key_file_path}") + })? + } else if import.private_key.is_none() && import.private_key_file_path.is_none() { + &get_private_key_from_input()? + } else { + unreachable!("Checked on clap level") + }; + let private_key = &SigningKey::from_secret_scalar(*private_key); + + let fetched_class_hash = get_class_hash_by_address(provider, import.address).await?; + let deployed: bool = fetched_class_hash.is_some(); + let class_hash = if let (Some(from_provider), Some(from_user)) = + (fetched_class_hash, import.class_hash) + { + ensure!( + from_provider == from_user, + "Incorrect class hash {:#x} for account address {:#x} was provided", + from_user, + import.address + ); + from_provider + } else if let Some(from_user) = import.class_hash { + check_class_hash_exists(provider, from_user).await?; + from_user + } else if let Some(from_provider) = fetched_class_hash { + from_provider + } else { + bail!( + "Class hash for the account address {:#x} could not be found. Please provide the class hash", + import.address + ); + }; + + let chain_id = get_chain_id(provider).await?; + if let Some(salt) = import.salt { + // TODO(#2571) + let sncast_account_type = match import.account_type { + AccountType::Argent => SNCastAccountType::Argent, + AccountType::Braavos => SNCastAccountType::Braavos, + AccountType::Oz => SNCastAccountType::OpenZeppelin, + }; + let computed_address = + compute_account_address(salt, private_key, class_hash, sncast_account_type, chain_id); + ensure!( + computed_address == import.address, + "Computed address {:#x} does not match the provided address {:#x}. Please ensure that the provided salt, class hash, and account type are correct.", + computed_address, + import.address + ); + } + + let legacy = check_if_legacy_contract(Some(class_hash), import.address, provider).await?; + + let account_json = prepare_account_json( + private_key, + import.address, + deployed, + legacy, + &import.account_type, + Some(class_hash), + import.salt, + ); + + write_account_to_accounts_file(account, accounts_file, chain_id, account_json.clone())?; + + if import.add_profile.is_some() { + let config = CastConfig { + url: import.rpc.url.clone().unwrap_or_default(), + account: account.into(), + accounts_file: accounts_file.into(), + ..Default::default() + }; + add_created_profile_to_configuration(&import.add_profile, &config, &None)?; + } + + Ok(AccountImportResponse { + add_profile: if import.add_profile.is_some() { + format!( + "Profile {} successfully added to snfoundry.toml", + import + .add_profile + .clone() + .expect("Failed to get profile name") + ) + } else { + "--add-profile flag was not set. No profile added to snfoundry.toml".to_string() + }, + }) +} + +fn get_private_key_from_file(file_path: &Utf8PathBuf) -> Result { + let private_key_string = std::fs::read_to_string(file_path.clone())?; + Ok(private_key_string.parse()?) +} + +fn parse_input_to_felt(input: &String) -> Result { + // Regex is from spec https://github.com/starkware-libs/starknet-specs/blob/6d88b7399f56260ece3821c71f9ce53ec55f830b/api/starknet_api_openrpc.json#L1303 + let felt_re = Regex::new(r"^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,62})$").unwrap(); + if input.starts_with("0x") && !felt_re.is_match(input) { + bail!( + "Failed to parse value {} to felt. Invalid hex value was passed", + input + ); + } else if let Ok(felt_from_hex) = Felt::try_from_hex_str(input) { + return Ok(felt_from_hex); + } else if let Ok(felt_from_dec) = Felt::try_from_dec_str(input) { + return Ok(felt_from_dec); + } + bail!("Failed to parse value {} to felt", input); +} + +fn get_private_key_from_input() -> Result { + let input = rpassword::prompt_password("Type in your private key and press enter: ") + .expect("Failed to read private key from input"); + parse_input_to_felt(&input) +} + +#[cfg(test)] +mod tests { + use crate::starknet_commands::account::import::parse_input_to_felt; + use conversions::string::TryFromHexStr; + use starknet::core::types::Felt; + + #[test] + fn test_parse_hex_str() { + let hex_str = "0x1a2b3c"; + let result = parse_input_to_felt(&hex_str.to_string()); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Felt::try_from_hex_str("0x1a2b3c").unwrap()); + } + + #[test] + fn test_parse_hex_str_invalid() { + let hex_str = "0xz"; + let result = parse_input_to_felt(&hex_str.to_string()); + + assert!(result.is_err()); + let error_message = result.unwrap_err().to_string(); + assert_eq!( + "Failed to parse value 0xz to felt. Invalid hex value was passed", + error_message + ); + } + + #[test] + fn test_parse_dec_str() { + let dec_str = "123"; + let result = parse_input_to_felt(&dec_str.to_string()); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Felt::from(123)); + } + + #[test] + fn test_parse_dec_str_negative() { + let dec_str = "-123"; + let result = parse_input_to_felt(&dec_str.to_string()); + + assert!(result.is_err()); + let error_message = result.unwrap_err().to_string(); + assert_eq!("Failed to parse value -123 to felt", error_message); + } + + #[test] + fn test_parse_invalid_str() { + let invalid_str = "invalid"; + let result = parse_input_to_felt(&invalid_str.to_string()); + + assert!(result.is_err()); + let error_message = result.unwrap_err().to_string(); + assert_eq!("Failed to parse value invalid to felt", error_message); + } +} diff --git a/crates/sncast/src/starknet_commands/account/mod.rs b/crates/sncast/src/starknet_commands/account/mod.rs index 64aefc797f..701d1d9d7e 100644 --- a/crates/sncast/src/starknet_commands/account/mod.rs +++ b/crates/sncast/src/starknet_commands/account/mod.rs @@ -1,7 +1,7 @@ -use crate::starknet_commands::account::add::Add; use crate::starknet_commands::account::create::Create; use crate::starknet_commands::account::delete::Delete; use crate::starknet_commands::account::deploy::Deploy; +use crate::starknet_commands::account::import::Import; use crate::starknet_commands::account::list::List; use anyhow::{anyhow, bail, Context, Result}; use camino::Utf8PathBuf; @@ -15,10 +15,10 @@ use starknet::{core::types::Felt, signers::SigningKey}; use std::{fmt, fs::OpenOptions, io::Write}; use toml::Value; -pub mod add; pub mod create; pub mod delete; pub mod deploy; +pub mod import; pub mod list; #[derive(Args)] @@ -30,7 +30,7 @@ pub struct Account { #[derive(Debug, Subcommand)] pub enum Commands { - Add(Add), + Import(Import), Create(Create), Deploy(Deploy), Delete(Delete), diff --git a/crates/sncast/tests/e2e/account/add.rs b/crates/sncast/tests/e2e/account/import.rs similarity index 78% rename from crates/sncast/tests/e2e/account/add.rs rename to crates/sncast/tests/e2e/account/import.rs index 76d6396828..de852e3f66 100644 --- a/crates/sncast/tests/e2e/account/add.rs +++ b/crates/sncast/tests/e2e/account/import.rs @@ -25,11 +25,11 @@ pub async fn test_happy_case(input_account_type: &str, saved_type: &str) { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", "0x123", "--private-key", @@ -43,7 +43,7 @@ pub async fn test_happy_case(input_account_type: &str, saved_type: &str) { let snapbox = runner(&args).current_dir(tempdir.path()); snapbox.assert().stdout_matches(indoc! {r" - command: account add + command: account import add_profile: --add-profile flag was not set. No profile added to snfoundry.toml "}); @@ -55,7 +55,7 @@ pub async fn test_happy_case(input_account_type: &str, saved_type: &str) { json!( { "alpha-sepolia": { - "my_account_add": { + "my_account_import": { "address": "0x123", "class_hash": DEVNET_OZ_CLASS_HASH_CAIRO_0, "deployed": false, @@ -79,11 +79,11 @@ pub async fn test_existent_account_address() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "--private-key", @@ -102,7 +102,7 @@ pub async fn test_existent_account_address() { json!( { "alpha-sepolia": { - "my_account_add": { + "my_account_import": { "address": DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "class_hash": &DEVNET_OZ_CLASS_HASH_CAIRO_1.into_hex_string(), "deployed": true, @@ -126,11 +126,11 @@ pub async fn test_existent_account_address_and_incorrect_class_hash() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "--private-key", @@ -144,8 +144,8 @@ pub async fn test_existent_account_address_and_incorrect_class_hash() { let snapbox = runner(&args).current_dir(tempdir.path()); snapbox.assert().stderr_matches(formatdoc! {r" - command: account add - error: Incorrect class hash {} for account address {} + command: account import + error: Incorrect class hash {} for account address {} was provided ", DEVNET_OZ_CLASS_HASH_CAIRO_0, DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS}); } @@ -158,11 +158,11 @@ pub async fn test_nonexistent_account_address_and_nonexistent_class_hash() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", "0x202", "--private-key", @@ -176,7 +176,7 @@ pub async fn test_nonexistent_account_address_and_nonexistent_class_hash() { let snapbox = runner(&args).current_dir(tempdir.path()); snapbox.assert().stderr_matches(indoc! {r" - command: account add + command: account import error: Class with hash 0x101 is not declared, try using --class-hash with a hash of the declared class "}); } @@ -190,11 +190,11 @@ pub async fn test_nonexistent_account_address() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", "0x123", "--private-key", @@ -206,8 +206,8 @@ pub async fn test_nonexistent_account_address() { let snapbox = runner(&args).current_dir(tempdir.path()); snapbox.assert().stderr_matches(indoc! {r" - command: account add - error: There is no contract at the specified address + command: account import + error: Class hash for the account address 0x123 could not be found. Please provide the class hash "}); } @@ -220,32 +220,28 @@ pub async fn test_happy_case_add_profile() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", "0x1", "--private-key", "0x2", - "--public-key", - "0x759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5", - "--salt", - "0x3", "--class-hash", DEVNET_OZ_CLASS_HASH_CAIRO_0, "--type", "oz", "--add-profile", - "my_account_add", + "my_account_import", ]; let snapbox = runner(&args).current_dir(tempdir.path()); snapbox.assert().stdout_matches(indoc! {r" - command: account add - add_profile: Profile my_account_add successfully added to snfoundry.toml + command: account import + add_profile: Profile my_account_import successfully added to snfoundry.toml "}); let current_dir_utf8 = Utf8PathBuf::try_from(tempdir.path().to_path_buf()).unwrap(); @@ -257,13 +253,12 @@ pub async fn test_happy_case_add_profile() { json!( { "alpha-sepolia": { - "my_account_add": { + "my_account_import": { "address": "0x1", "class_hash": DEVNET_OZ_CLASS_HASH_CAIRO_0, "deployed": false, "private_key": "0x2", "public_key": "0x759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5", - "salt": "0x3", "legacy": true, "type": "open_zeppelin" } @@ -274,8 +269,8 @@ pub async fn test_happy_case_add_profile() { let contents = fs::read_to_string(current_dir_utf8.join("snfoundry.toml")) .expect("Unable to read snfoundry.toml"); - assert!(contents.contains("[sncast.my_account_add]")); - assert!(contents.contains("account = \"my_account_add\"")); + assert!(contents.contains("[sncast.my_account_import]")); + assert!(contents.contains("account = \"my_account_import\"")); assert!(contents.contains(&format!("url = \"{URL}\""))); } @@ -288,11 +283,11 @@ pub async fn test_detect_deployed() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "--private-key", @@ -304,7 +299,7 @@ pub async fn test_detect_deployed() { let snapbox = runner(&args).current_dir(tempdir.path()); snapbox.assert().stdout_matches(indoc! {r" - command: account add + command: account import add_profile: --add-profile flag was not set. No profile added to snfoundry.toml "}); @@ -316,7 +311,7 @@ pub async fn test_detect_deployed() { json!( { "alpha-sepolia": { - "my_account_add": { + "my_account_import": { "address": DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "class_hash": &DEVNET_OZ_CLASS_HASH_CAIRO_1.into_hex_string(), "deployed": true, @@ -332,40 +327,16 @@ pub async fn test_detect_deployed() { } #[tokio::test] -pub async fn test_invalid_public_key() { +pub async fn test_missing_arguments() { let args = vec![ "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", - "--address", - "0x123", - "--private-key", - "0x456", - "--public-key", - "0x457", - "--type", - "oz", + "my_account_import", ]; - let snapbox = runner(&args); - let output = snapbox.assert().success(); - - assert_stderr_contains( - output, - indoc! {r" - command: account add - error: The private key does not match the public key - "}, - ); -} - -#[tokio::test] -pub async fn test_missing_arguments() { - let args = vec!["account", "add", "--url", URL, "--name", "my_account_add"]; - let snapbox = runner(&args); let output = snapbox.assert().failure(); @@ -375,7 +346,6 @@ pub async fn test_missing_arguments() { error: the following required arguments were not provided: --address
--type - <--private-key |--private-key-file > "}, ); } @@ -392,11 +362,11 @@ pub async fn test_private_key_from_file() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", "0x123", "--private-key-file", @@ -410,7 +380,7 @@ pub async fn test_private_key_from_file() { let snapbox = runner(&args).current_dir(temp_dir.path()); snapbox.assert().stdout_matches(indoc! {r" - command: account add + command: account import add_profile: --add-profile flag was not set. No profile added to snfoundry.toml "}); @@ -422,7 +392,7 @@ pub async fn test_private_key_from_file() { json!( { "alpha-sepolia": { - "my_account_add": { + "my_account_import": { "address": "0x123", "deployed": false, "legacy": true, @@ -441,9 +411,9 @@ pub async fn test_private_key_from_file() { pub async fn test_accept_only_one_private_key() { let args = vec![ "account", - "add", + "import", "--name", - "my_account_add", + "my_account_import", "--address", "0x123", "--private-key", @@ -465,11 +435,11 @@ pub async fn test_accept_only_one_private_key() { pub async fn test_invalid_private_key_file_path() { let args = vec![ "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", "0x123", "--private-key-file", @@ -484,7 +454,7 @@ pub async fn test_invalid_private_key_file_path() { assert_stderr_contains( output, indoc! {r" - command: account add + command: account import error: Failed to obtain private key from the file my_private_key: No such file or directory (os error 2) "}, ); @@ -505,11 +475,11 @@ pub async fn test_invalid_private_key_in_file() { "--accounts-file", "accounts.json", "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", "0x123", "--private-key-file", @@ -524,7 +494,7 @@ pub async fn test_invalid_private_key_in_file() { assert_stderr_contains( output, indoc! {r" - command: account add + command: account import error: Failed to obtain private key from the file my_private_key: Failed to create Felt from string "}, ); @@ -542,11 +512,11 @@ pub async fn test_private_key_as_int_in_file() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "--private-key-file", @@ -568,7 +538,7 @@ pub async fn test_private_key_as_int_in_file() { json!( { "alpha-sepolia": { - "my_account_add": { + "my_account_import": { "address": DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "deployed": true, "legacy": false, @@ -593,11 +563,11 @@ pub async fn test_empty_config_add_profile() { "--accounts-file", accounts_file, "account", - "add", + "import", "--url", URL, "--name", - "my_account_add", + "my_account_import", "--address", DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, "--private-key", @@ -611,7 +581,7 @@ pub async fn test_empty_config_add_profile() { let snapbox = runner(&args).current_dir(tempdir.path()); snapbox.assert().stdout_matches(indoc! {r" - command: account add + command: account import add_profile: Profile random successfully added to snfoundry.toml "}); let current_dir_utf8 = Utf8PathBuf::try_from(tempdir.path().to_path_buf()).unwrap(); @@ -619,6 +589,97 @@ pub async fn test_empty_config_add_profile() { let contents = fs::read_to_string(current_dir_utf8.join("snfoundry.toml")) .expect("Unable to read snfoundry.toml"); assert!(contents.contains("[sncast.random]")); - assert!(contents.contains("account = \"my_account_add\"")); + assert!(contents.contains("account = \"my_account_import\"")); assert!(contents.contains(&format!("url = \"{URL}\""))); } + +#[tokio::test] +pub async fn test_happy_case_valid_address_computation() { + let tempdir = tempdir().expect("Unable to create a temporary directory"); + let accounts_file = "accounts.json"; + + let args = vec![ + "--accounts-file", + accounts_file, + "account", + "import", + "--url", + URL, + "--name", + "my_account_import", + "--address", + "0x721c21e0cc9d37aec8e176797effd1be222aff6db25f068040adefabb7cfb6d", + "--private-key", + "0x2", + "--salt", + "0x3", + "--class-hash", + DEVNET_OZ_CLASS_HASH_CAIRO_0, + "--type", + "oz", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + + snapbox.assert().stdout_matches(indoc! {r" + command: account import + add_profile: --add-profile flag was not set. No profile added to snfoundry.toml + "}); + + let contents = fs::read_to_string(tempdir.path().join(accounts_file)) + .expect("Unable to read created file"); + let contents_json: serde_json::Value = serde_json::from_str(&contents).unwrap(); + assert_eq!( + contents_json, + json!( + { + "alpha-sepolia": { + "my_account_import": { + "address": "0x721c21e0cc9d37aec8e176797effd1be222aff6db25f068040adefabb7cfb6d", + "class_hash": DEVNET_OZ_CLASS_HASH_CAIRO_0, + "deployed": false, + "salt": "0x3", + "legacy": true, + "private_key": "0x2", + "public_key": "0x759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5", + "type": "open_zeppelin" + } + } + } + ) + ); +} + +#[tokio::test] +pub async fn test_invalid_address_computation() { + let tempdir = tempdir().expect("Unable to create a temporary directory"); + let accounts_file = "accounts.json"; + + let args = vec![ + "--accounts-file", + accounts_file, + "account", + "import", + "--url", + URL, + "--name", + "my_account_import", + "--address", + "0x123", + "--private-key", + "0x456", + "--salt", + "0x789", + "--class-hash", + DEVNET_OZ_CLASS_HASH_CAIRO_0, + "--type", + "oz", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let computed_address = "0xaf550326d32c8106ef08d25cbc0dba06e5cd10a2201c2e9bc5ad4cbbce45e6"; + snapbox.assert().stderr_matches(formatdoc! {r" + command: account import + error: Computed address {computed_address} does not match the provided address 0x123. Please ensure that the provided salt, class hash, and account type are correct. + "}); +} diff --git a/crates/sncast/tests/e2e/account/mod.rs b/crates/sncast/tests/e2e/account/mod.rs index 1b6dd472eb..66dac0c8e5 100644 --- a/crates/sncast/tests/e2e/account/mod.rs +++ b/crates/sncast/tests/e2e/account/mod.rs @@ -1,6 +1,6 @@ -mod add; mod create; mod delete; mod deploy; mod helpers; +mod import; mod list; diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index fe4b4a63be..dda87d3fbf 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -41,6 +41,7 @@ * [Outline](starknet/index.md) * [Creating And Deploying Accounts](starknet/account.md) +* [Importing Accounts](starknet/account-import.md) * [Declaring New Contracts](starknet/declare.md) * [Deploying New Contracts](starknet/deploy.md) * [Invoking Contracts](starknet/invoke.md) @@ -104,7 +105,7 @@ * [`sncast` Commands](appendix/sncast.md) * [common flags](appendix/sncast/common.md) * [account](appendix/sncast/account/account.md) - * [add](appendix/sncast/account/add.md) + * [import](appendix/sncast/account/import.md) * [create](appendix/sncast/account/create.md) * [deploy](appendix/sncast/account/deploy.md) * [delete](appendix/sncast/account/delete.md) diff --git a/docs/src/appendix/sncast.md b/docs/src/appendix/sncast.md index 3d408e3830..fdd658795f 100644 --- a/docs/src/appendix/sncast.md +++ b/docs/src/appendix/sncast.md @@ -2,7 +2,7 @@ * [common flags](./sncast/common.md) * [account](./sncast/account/account.md) - * [add](./sncast/account/add.md) + * [import](./sncast/account/import.md) * [create](./sncast/account/create.md) * [deploy](./sncast/account/deploy.md) * [delete](./sncast/account/delete.md) diff --git a/docs/src/appendix/sncast/account/account.md b/docs/src/appendix/sncast/account/account.md index c8205937cf..6de8d31e42 100644 --- a/docs/src/appendix/sncast/account/account.md +++ b/docs/src/appendix/sncast/account/account.md @@ -2,7 +2,7 @@ Provides a set of account management commands. It has the following subcommands: -* [`add`](./add.md) +* [`import`](./import.md) * [`create`](./create.md) * [`deploy`](./deploy.md) * [`delete`](./delete.md) diff --git a/docs/src/appendix/sncast/account/add.md b/docs/src/appendix/sncast/account/import.md similarity index 77% rename from docs/src/appendix/sncast/account/add.md rename to docs/src/appendix/sncast/account/import.md index 946d2370d5..b54b000600 100644 --- a/docs/src/appendix/sncast/account/add.md +++ b/docs/src/appendix/sncast/account/import.md @@ -1,4 +1,4 @@ -# `add` +# `import` Import an account to accounts file. Account information will be saved to the file specified by `--accounts-file` argument, @@ -7,7 +7,7 @@ which is `~/.starknet_accounts/starknet_open_zeppelin_accounts.json` by default. ## `--name, -n ` Required. -Name of the account to be added. +Name of the account to be imported. ## `--address, -a
` Required. @@ -32,21 +32,15 @@ Optional. Class hash of the account. ## `--private-key ` -Optional. Required if `--private-key-file` is not passed. +Optional. Account private key. ## `--private-key-file ` -Optional. Required if `--private-key-file` is not passed. +Optional. If neither `--private-key` nor `--private-key-file` is passed, the user will be prompted to enter the account private key. Path to the file holding account private key. -## `--public-key ` -Optional. - -Account public key. -If not passed, will be computed from `--private-key`. - ## `--salt, -s ` Optional. diff --git a/docs/src/starknet/account-import.md b/docs/src/starknet/account-import.md new file mode 100644 index 0000000000..b5f79101ed --- /dev/null +++ b/docs/src/starknet/account-import.md @@ -0,0 +1,139 @@ +# Importing Accounts + +You can export your private key from wallet (Argent, Braavos) and import it into the file holding the accounts info (`~/.starknet_accounts/starknet_open_zeppelin_accounts.json` by default). + +## Exporting Your Private Key + +This section shows how to export your private key from specific wallets. + +### Examples + +#### Argent + +1. Open the Argent app > Settings. +
+
+ + +2. Click on the current account. +
+
+ + +3. Click on "Export private key". +
+
+ + +4. Enter your password. +
+
+ + +5. Copy your private key. +
+
+ + + +#### Braavos + +1. Open the Braavos app > Wallet settings. +
+
+ + +2. Click on "Privacy & Security". +
+
+ + +3. Click on "Export private key". +
+
+ + +4. Enter your password. +
+
+ + +5. Copy your private key. +
+
+ + +## Importing an Account + +### Examples + +#### General Example + +To import an account into the file holding the accounts info (`~/.starknet_accounts/starknet_open_zeppelin_accounts.json` by default), use the `account import` command. + +```shell +$ sncast \ + account import \ + --url http://127.0.0.1:5050 \ + --name account_123 \ + --address 0x1 \ + --private-key 0x2 \ + --type oz +``` + +#### Passing Private Key in an Interactive + +If you don't want to pass the private key in the command (because of safety aspect), you can skip `--private-key` flag. You will be prompted to enter the private key in interactive mode. + +```shell +$ sncast \ + account import \ + --url http://127.0.0.1:5050 \ + --name account_123 \ + --address 0x1 \ + --type oz + +Type in your private key and press enter: +``` + +#### Argent + +To import Argent account, set the `--type` flag to `argent`. + +```shell +$ sncast \ + account import \ + --url http://127.0.0.1:5050 \ + --name account_argent \ + --address 0x1 \ + --private-key 0x2 \ + --type argent +``` + +#### Braavos + +To import Braavos account, set the `--type` flag to `braavos`. + +```shell +$ sncast \ + account import \ + --url http://127.0.0.1:5050 \ + --name account_braavos \ + --address 0x1 \ + --private-key 0x2 \ + --type braavos +``` + +#### OpenZeppelin + +To import OpenZeppelin account, set the `--type` flag to `oz` or `open_zeppelin`. + +```shell +$ sncast \ + account import \ + --url http://127.0.0.1:5050 \ + --name account_oz \ + --address 0x1 \ + --private-key 0x2 \ + --type oz +``` \ No newline at end of file diff --git a/docs/src/starknet/account.md b/docs/src/starknet/account.md index 709850d9fc..c30ecc72b0 100644 --- a/docs/src/starknet/account.md +++ b/docs/src/starknet/account.md @@ -196,23 +196,6 @@ $ sncast \ --contract-name my_contract ``` -#### Importing an Account - -To import an account into the file holding the accounts info (`~/.starknet_accounts/starknet_open_zeppelin_accounts.json` by default), use the `account add` command. - -```shell -$ sncast \ - account add \ - --url http://127.0.0.1:5050 \ - --name my_imported_account \ - --address 0x1 \ - --private-key 0x2 \ - --class-hash 0x3 \ - --type oz -``` - -For a detailed CLI description, see [account add command reference](../appendix/sncast/account/add.md). - ### Creating an Account With Starkli-Style Keystore It is possible to create an openzeppelin account with keystore in a similar way [starkli](https://book.starkli.rs/accounts#accounts) does. diff --git a/docs/src/starknet/img/argent_export_1.png b/docs/src/starknet/img/argent_export_1.png new file mode 100644 index 0000000000..9b933746c8 Binary files /dev/null and b/docs/src/starknet/img/argent_export_1.png differ diff --git a/docs/src/starknet/img/argent_export_2.png b/docs/src/starknet/img/argent_export_2.png new file mode 100644 index 0000000000..67697dd2cc Binary files /dev/null and b/docs/src/starknet/img/argent_export_2.png differ diff --git a/docs/src/starknet/img/argent_export_3.png b/docs/src/starknet/img/argent_export_3.png new file mode 100644 index 0000000000..bf523a21db Binary files /dev/null and b/docs/src/starknet/img/argent_export_3.png differ diff --git a/docs/src/starknet/img/argent_export_4.png b/docs/src/starknet/img/argent_export_4.png new file mode 100644 index 0000000000..9496ead849 Binary files /dev/null and b/docs/src/starknet/img/argent_export_4.png differ diff --git a/docs/src/starknet/img/argent_export_5.png b/docs/src/starknet/img/argent_export_5.png new file mode 100644 index 0000000000..aaee95a89f Binary files /dev/null and b/docs/src/starknet/img/argent_export_5.png differ diff --git a/docs/src/starknet/img/braavos_export_1.png b/docs/src/starknet/img/braavos_export_1.png new file mode 100644 index 0000000000..5b950b3839 Binary files /dev/null and b/docs/src/starknet/img/braavos_export_1.png differ diff --git a/docs/src/starknet/img/braavos_export_2.png b/docs/src/starknet/img/braavos_export_2.png new file mode 100644 index 0000000000..cd30dad912 Binary files /dev/null and b/docs/src/starknet/img/braavos_export_2.png differ diff --git a/docs/src/starknet/img/braavos_export_3.png b/docs/src/starknet/img/braavos_export_3.png new file mode 100644 index 0000000000..24129b60ad Binary files /dev/null and b/docs/src/starknet/img/braavos_export_3.png differ diff --git a/docs/src/starknet/img/braavos_export_4.png b/docs/src/starknet/img/braavos_export_4.png new file mode 100644 index 0000000000..3c24fbe6f1 Binary files /dev/null and b/docs/src/starknet/img/braavos_export_4.png differ diff --git a/docs/src/starknet/img/braavos_export_5.png b/docs/src/starknet/img/braavos_export_5.png new file mode 100644 index 0000000000..3fc5bb4792 Binary files /dev/null and b/docs/src/starknet/img/braavos_export_5.png differ