diff --git a/crates/sncast/src/lib.rs b/crates/sncast/src/lib.rs index 042eb95fb0..dcbb0baa56 100644 --- a/crates/sncast/src/lib.rs +++ b/crates/sncast/src/lib.rs @@ -69,6 +69,12 @@ impl AccountData { Self::get_as_felt(&self.class_hash, "class_hash") } + pub fn get_account_type(&self) -> Result { + self.account_type + .clone() + .ok_or_else(|| anyhow!("Failed to get account type")) + } + fn get_as_felt(value: &Option, key_name: &str) -> Result { let value_to_parse = value .as_ref() @@ -325,9 +331,6 @@ pub fn get_account_data_from_keystore( .map(str::to_string) }; - let public_key = parse_to_string(&account_info, "variant", "public_key") - .ok_or_else(|| anyhow::anyhow!("Failed to get public key from account JSON file"))?; - let address = parse_to_string(&account_info, "deployment", "address"); let class_hash = parse_to_string(&account_info, "deployment", "class_hash"); let salt = parse_to_string(&account_info, "deployment", "salt"); @@ -336,6 +339,18 @@ pub fn get_account_data_from_keystore( let legacy = get_from_json(&account_info, "variant", "legacy").and_then(Value::as_bool); let account_type = parse_to_string(&account_info, "variant", "type"); + let public_key_field = match account_type + .clone() + .ok_or_else(|| anyhow!("Failed to get type key"))? + .as_str() + { + "argent" => "owner", + _ => "public_key", + }; + + let public_key = parse_to_string(&account_info, "variant", public_key_field) + .ok_or_else(|| anyhow::anyhow!("Failed to get public key from account JSON file"))?; + Ok(AccountData { private_key, public_key, diff --git a/crates/sncast/src/starknet_commands/account/deploy.rs b/crates/sncast/src/starknet_commands/account/deploy.rs index dcd365655d..448730ebcc 100644 --- a/crates/sncast/src/starknet_commands/account/deploy.rs +++ b/crates/sncast/src/starknet_commands/account/deploy.rs @@ -4,8 +4,8 @@ use clap::Args; use serde_json::Map; use sncast::helpers::constants::KEYSTORE_PASSWORD_ENV_VAR; use sncast::response::structs::{Felt, InvokeResponse}; -use starknet::accounts::AccountFactoryError; use starknet::accounts::{AccountFactory, OpenZeppelinAccountFactory}; +use starknet::accounts::{AccountFactoryError, ArgentAccountFactory}; use starknet::core::types::BlockTag::Pending; use starknet::core::types::{BlockId, FieldElement, StarknetError::ClassHashNotFound}; use starknet::core::utils::get_contract_address; @@ -100,12 +100,15 @@ async fn deploy_from_keystore( let salt = account_data.get_salt_as_felt()?; let class_hash = account_data.get_class_hash_as_felt()?; - let address = get_contract_address( - salt, - class_hash, - &[private_key.verifying_key().scalar()], - FieldElement::ZERO, - ); + // TODO: Improve code for checking if address exists or remove this logic + let account_type = &account_data.get_account_type()?; + let calldata = match account_type.as_str() { + "open_zeppelin" => vec![private_key.verifying_key().scalar()], + "argent" => vec![private_key.verifying_key().scalar(), FieldElement::ZERO], + _ => panic!("Invalid account type"), + }; + + let address = get_contract_address(salt, class_hash, &calldata, FieldElement::ZERO); let result = if provider .get_class_hash_at(BlockId::Tag(Pending), address) @@ -116,8 +119,9 @@ async fn deploy_from_keystore( transaction_hash: Felt(FieldElement::ZERO), } } else { - deploy_oz_account( + get_deployment_result( provider, + account_type.as_str(), class_hash, private_key, salt, @@ -147,8 +151,9 @@ async fn deploy_from_accounts_file( parse_number(&account_data.private_key).context("Failed to parse private key")?, ); - let result = deploy_oz_account( + let result = get_deployment_result( provider, + &account_data.get_account_type()?, account_data.get_class_hash_as_felt()?, private_key, account_data.get_salt_as_felt()?, @@ -163,9 +168,51 @@ async fn deploy_from_accounts_file( Ok(result) } +#[allow(clippy::too_many_arguments)] +async fn get_deployment_result( + provider: &JsonRpcClient, + account_type: &str, + class_hash: FieldElement, + private_key: SigningKey, + salt: FieldElement, + chain_id: FieldElement, + max_fee: Option, + wait_config: WaitForTx, +) -> Result { + match account_type { + "argent" => { + deploy_argent_account( + provider, + class_hash, + private_key, + salt, + chain_id, + max_fee, + wait_config, + ) + .await + } + "open_zeppelin" => { + deploy_oz_account( + provider, + class_hash, + private_key, + salt, + chain_id, + max_fee, + wait_config, + ) + .await + } + _ => Err(anyhow!( + "Incorrect account type, possible values are ['open_zeppelin', 'argent']" + )), + } +} + async fn deploy_oz_account( provider: &JsonRpcClient, - oz_class_hash: FieldElement, + class_hash: FieldElement, private_key: SigningKey, salt: FieldElement, chain_id: FieldElement, @@ -173,24 +220,56 @@ async fn deploy_oz_account( wait_config: WaitForTx, ) -> Result { let factory = OpenZeppelinAccountFactory::new( - oz_class_hash, + class_hash, + chain_id, + LocalWallet::from_signing_key(private_key), + provider, + ) + .await?; + + deploy_account(factory, provider, salt, max_fee, wait_config, class_hash).await +} + +async fn deploy_argent_account( + provider: &JsonRpcClient, + class_hash: FieldElement, + private_key: SigningKey, + salt: FieldElement, + chain_id: FieldElement, + max_fee: Option, + wait_config: WaitForTx, +) -> Result { + let factory = ArgentAccountFactory::new( + class_hash, chain_id, + FieldElement::ZERO, LocalWallet::from_signing_key(private_key), provider, ) .await?; - let deployment = factory.deploy(salt); + deploy_account(factory, provider, salt, max_fee, wait_config, class_hash).await +} + +async fn deploy_account( + account_factory: T, + provider: &JsonRpcClient, + salt: FieldElement, + max_fee: Option, + wait_config: WaitForTx, + class_hash: FieldElement, +) -> Result +where + T: AccountFactory + Sync, +{ + let deployment = account_factory.deploy(salt); + let deploy_max_fee = if let Some(max_fee) = max_fee { max_fee } else { match deployment.estimate_fee().await { Ok(max_fee) => max_fee.overall_fee, - Err(error) => { - return Err(handle_account_factory_error::< - OpenZeppelinAccountFactory>, - >(error)) - } + Err(error) => return Err(handle_account_factory_error::(error)), } }; let result = deployment.max_fee(deploy_max_fee).send().await; @@ -199,7 +278,7 @@ async fn deploy_oz_account( Err(AccountFactoryError::Provider(error)) => match error { StarknetError(ClassHashNotFound) => Err(anyhow!( "Provided class hash {:#x} does not exist", - oz_class_hash, + class_hash, )), _ => Err(handle_rpc_error(error)), }, diff --git a/crates/sncast/tests/data/keystore/my_account_argent_undeployed_happy_case.json b/crates/sncast/tests/data/keystore/my_account_argent_undeployed_happy_case.json new file mode 100644 index 0000000000..61f8c9174f --- /dev/null +++ b/crates/sncast/tests/data/keystore/my_account_argent_undeployed_happy_case.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "variant": { + "type": "argent", + "version": 1, + "owner": "0xe2d3d7080bfc665e0060a06e8e95c3db3ff78a1fec4cc81ddc87e49a12e0a", + "guardian": "0x0" + }, + "deployment": { + "status": "undeployed", + "class_hash": "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b", + "salt": "0x296a50ef3b2272b97072186f2905ae6fb7a7bde2d4f2c1f1375c324fc529b9b" + } +} diff --git a/crates/sncast/tests/data/keystore/my_account_undeployed_happy_case.json b/crates/sncast/tests/data/keystore/my_account_oz_undeployed_happy_case.json similarity index 100% rename from crates/sncast/tests/data/keystore/my_account_undeployed_happy_case.json rename to crates/sncast/tests/data/keystore/my_account_oz_undeployed_happy_case.json diff --git a/crates/sncast/tests/e2e/account/deploy.rs b/crates/sncast/tests/e2e/account/deploy.rs index df325dc7f8..13dc14446e 100644 --- a/crates/sncast/tests/e2e/account/deploy.rs +++ b/crates/sncast/tests/e2e/account/deploy.rs @@ -1,4 +1,6 @@ -use crate::helpers::constants::{DEVNET_OZ_CLASS_HASH_CAIRO_0, DEVNET_OZ_CLASS_HASH_CAIRO_1, URL}; +use crate::helpers::constants::{ + ARGENT_ACCOUNT_CLASS_HASH, DEVNET_OZ_CLASS_HASH_CAIRO_0, DEVNET_OZ_CLASS_HASH_CAIRO_1, URL, +}; use crate::helpers::fixtures::copy_file; use crate::helpers::fixtures::{ get_address_from_keystore, get_transaction_hash, get_transaction_receipt, mint_token, @@ -17,6 +19,7 @@ use test_case::test_case; #[test_case(DEVNET_OZ_CLASS_HASH_CAIRO_0, "oz"; "cairo_0_class_hash")] #[test_case(DEVNET_OZ_CLASS_HASH_CAIRO_1, "oz"; "cairo_1_class_hash")] +#[test_case(ARGENT_ACCOUNT_CLASS_HASH, "argent"; "argent_class_hash")] #[tokio::test] pub async fn test_happy_case(class_hash: &str, account_type: &str) { let tempdir = create_account(false, class_hash, account_type).await; @@ -88,7 +91,6 @@ pub async fn test_happy_case_add_profile() { #[test_case("{\"alpha-sepolia\": {}}", "error: Account = my_account not found under network = alpha-sepolia" ; "when account name not present")] #[test_case("{\"alpha-sepolia\": {\"my_account\" : {}}}", "error: Failed to parse file = accounts.json to JSON: missing field `private_key`[..]" ; "when private key not present")] -#[test_case("{\"alpha-sepolia\": {\"my_account\" : {\"private_key\": \"0x1\", \"public_key\": \"0x2\", \"class_hash\": \"0x3\"}}}", "error: Failed to get salt" ; "when salt not present")] fn test_account_deploy_error(accounts_content: &str, error: &str) { let temp_dir = tempdir().expect("Unable to create a temporary directory"); @@ -236,27 +238,30 @@ pub async fn create_account(add_profile: bool, class_hash: &str, account_type: & tempdir } +#[test_case("oz"; "open_zeppelin_account")] +#[test_case("argent"; "argent_account")] #[tokio::test] -pub async fn test_happy_case_keystore() { +pub async fn test_happy_case_keystore(account_type: &str) { let tempdir = tempdir().expect("Unable to create a temporary directory"); let keystore_file = "my_key.json"; - let account_file = "my_account_undeployed_happy_case.json"; + let account_file = format!("my_account_{account_type}_undeployed_happy_case.json"); copy_file( "tests/data/keystore/my_key.json", tempdir.path().join(keystore_file), ); copy_file( - "tests/data/keystore/my_account_undeployed_happy_case.json", - tempdir.path().join(account_file), + format!("tests/data/keystore/{account_file}"), + tempdir.path().join(&account_file), ); env::set_var(KEYSTORE_PASSWORD_ENV_VAR, "123"); let address = get_address_from_keystore( tempdir.path().join(keystore_file).to_str().unwrap(), - tempdir.path().join(account_file).to_str().unwrap(), + tempdir.path().join(&account_file).to_str().unwrap(), KEYSTORE_PASSWORD_ENV_VAR, + account_type, ); mint_token(&address.into_hex_string(), 9_999_999_999_999_999_999).await; @@ -267,7 +272,7 @@ pub async fn test_happy_case_keystore() { "--keystore", keystore_file, "--account", - account_file, + &account_file, "account", "deploy", "--max-fee", @@ -513,6 +518,7 @@ pub async fn test_deploy_keystore_other_args() { tempdir.path().join(keystore_file), tempdir.path().join(account_file), KEYSTORE_PASSWORD_ENV_VAR, + "oz", ); mint_token(&address.into_hex_string(), 9_999_999_999_999_999_999).await; diff --git a/crates/sncast/tests/helpers/fixtures.rs b/crates/sncast/tests/helpers/fixtures.rs index 8ee312cee7..0a2f163f2c 100644 --- a/crates/sncast/tests/helpers/fixtures.rs +++ b/crates/sncast/tests/helpers/fixtures.rs @@ -432,6 +432,7 @@ pub fn get_address_from_keystore( keystore_path: impl AsRef, account_path: impl AsRef, password: &str, + account_type: &str, ) -> FieldElement { let contents = std::fs::read_to_string(account_path).unwrap(); let items: Map = serde_json::from_str(&contents).unwrap(); @@ -457,12 +458,13 @@ pub fn get_address_from_keystore( ) .unwrap(); - get_contract_address( - salt, - oz_class_hash, - &[private_key.verifying_key().scalar()], - FieldElement::ZERO, - ) + let calldata = match account_type { + "oz" => vec![private_key.verifying_key().scalar()], + "argent" => vec![private_key.verifying_key().scalar(), FieldElement::ZERO], + _ => panic!("Invalid account type"), + }; + + get_contract_address(salt, oz_class_hash, &calldata, FieldElement::ZERO) } #[must_use] pub fn get_accounts_path(relative_path_from_cargo_toml: &str) -> String {